Compare commits

...

61 Commits

Author SHA1 Message Date
Jan 7b67a5d81b turn window_renderer into managed pointer (#1683)
* turn window_renderer into managed pointer
this will make it easier to move it into userdata in the future

* remove unused function, remove comment
2023-12-26 13:16:33 +00:00
Adam Harrison cca61ab8ec Fixed a minor bug, should close issue #1680. 2023-12-26 13:16:33 +00:00
ThaCuber c45463459c fix nagbar failed save message (#1678)
* fix nagbar failed save message

- visually separated statements with a `.`
- first statement slightly rewritten
- use `'` rather than `"`

* yeahhhh no back to `"`
2023-12-26 13:16:33 +00:00
Guldoman 39993a6d93 Expose plaintext syntax (#1652) 2023-12-26 13:16:33 +00:00
Guldoman a29327e375 Use `\r\n` for new files on Windows (#1596)
* Use `\r\n` for new files on Windows

* Add `config.line_endings`
2023-12-26 13:16:33 +00:00
Takase 7111b8a6c9 feat(process): allow commands and envs on proces_start (#1477)
* feat(process): allow commands and envs on proces_start

* refactor(process): copy process arguments once whenever possible

Refactors the code to use an arglist type which is just lpCmdline on Windows
and a list in Linux.
The function automatically escapes the command when it is needed, avoiding
a second copy.

This also allows UTF-8 commands btw.

* fix(process): fix invalid dereference

* refactor(process): mark xstrdup as potentially unused

* feat(process): add parent process environment when launching process

* fix(process): fix operator precedence with array operators

* fix(process): fix segfault when freeing random memory

* fix(process): fix wrong check for setenv()

* fix(process): fix accidentally initializing an array by assignment

* fix(process): clear return value if success
2023-12-26 13:16:33 +00:00
takase1121 a9ac33429e chore(deps): update Lua 2023-12-26 13:16:33 +00:00
takase1121 ad1fad2632 chore(deps): update SDL2 2023-12-26 13:16:33 +00:00
takase1121 79bae532b9 chore(deps): update pcre2 2023-12-26 13:16:33 +00:00
takase1121 41813604e1 chore(deps): update freetype 2023-12-26 13:16:33 +00:00
takase1121 7b064bae6b fix(ci,build.sh): un-hardcode lua subproject detection 2023-12-26 13:16:33 +00:00
Takase 2d36359e6e Revert "feat(subprojects): update wraps (#1577)"
This reverts commit a97de87d869c227c2d41595d76ecafdc29e76bef.
2023-12-26 13:16:33 +00:00
Guldoman dac8d1ac8e Improve font/color change detection in `language_md` (#1614)
* Delay setting font for custom `language_md` token types

* Improve font/color change detection in `language_md`
2023-12-26 13:16:33 +00:00
Guldoman 5719f4de6f Use x offset to define render command rect in `rencache_draw_text` (#1618)
* Return x offset for the first character in `ren_font_group_get_width`

* Use x offset to define render command rect in `rencache_draw_text`
2023-12-26 13:16:33 +00:00
Adam e14af4604a Reverted cursor API to something more compatible with old API. (#1674)
* Reverted cursor API to something more compatible with old API.

* Implemented discord discussion.

* Reduced thiccness of overwrite cursor.
2023-12-26 13:16:33 +00:00
ThaCuber f43cfc4a94 Text overwriting (#1495)
* added text overwriting

* rewrote `DocView:draw_caret` to not use the order of draws

* forgot to delete some old code in `DocView:draw_overlay`
also added a temporary solution to overwriting
and added the missing arguments in `DocView:draw_ime_decoration`
and fixed `DocView:draw_caret`

* accidentally broke the `draw_caret` call in `draw_overlay` in the process

* multiline

* fixed calling `Doc:get_char` as a function
that, in turn, crashed the editor because "can't index a number"

* move and rename some stuff

* remove unneeded extra check

I just had to change the `~=` to `<` in the second condition

* overwrite disregards pasting text

* disregard overwrite on selections; doc only removes selection

* Fixed error where `doc` was used, instead of `self`.

---------

Co-authored-by: ThaCuber <70547062+ThaCuber@users.noreply.github.com>
Co-authored-by: Adam Harrison <adamdharrison@gmail.com>
2023-12-26 13:16:33 +00:00
Guldoman 234dd40e49 Fix patterns starting with `^` in `tokenizer` (#1645)
Previously the "dirty" version of the pattern was used, which could 
result in trying to match with multiple `^`, which failed valid matches.
2023-12-26 13:16:33 +00:00
Guldoman ee02d0e0b6 Fix `language_js` regex constant detection (#1581)
* Fix `language_js` regex constant detection

* Simplify regex constant detection in `language_js`

* Add more possessive quantifiers in `language_js` regex constant detection

This avoids more catastrophic backtracking cases.

* Allow `.` after regex constant in `language_js`
2023-12-26 13:16:33 +00:00
Guldoman de043f2e13 Fix editing after undo not clearing the change id (#1574) 2023-12-26 13:16:33 +00:00
Guldoman 9301220d26 Fix selecting newlines with `find-replace:select-add-{next,all}` (#1608)
* Avoid adding existing selections in `select_add_next`

* Use the first available selection as delimiter in `select_add_next`

* Fix returning searches with newlines in `search.find`

* Fix repeat search when the last result spanned multiple lines
2023-12-26 13:16:33 +00:00
Guldoman 2571e17d1b Fix `core.redraw` when window is not focused (#1601)
* Execute at least one step when window has no focus

This way if `core.redraw` is set, it's respected.

* Fully run threads at least once when window has no focus

This allows threads that set `core.redraw` (like `projectsearch`) to 
continue running even after the window loses focus.

"Fully" here means that `run_threads` has gone through *all* the "timed 
out" coroutines at least once.
2023-12-26 13:16:33 +00:00
Guldoman 4e2f70e5ee Scale mouse coordinates by window scale (#1630)
* Update window scale on resize

* Scale mouse coordinates by window scale

* Avoid scaling mouse coordinates while using `LITE_USE_SDL_RENDERER`
2023-12-26 13:16:33 +00:00
Takase 52d224ac6b feat(subprojects): update wraps (#1577)
* feat(subprojects): update SDL2 wrap

* fix(meson.build): add sdl2main as dependency on Windows

* fix(meson.build): don't load sdl2main on non-Windows platforms

* feat(subprojects): update freetype version

* feat(subprojects): update pcre2 to latest version

* feat(subprojects): update lua to latest version

* feat(lite_xl_plugin_api): add lua_closethread to symbols list

* fix(meson.build): fix meson error with features and booleans

* fix(meson.build): fix wrong variable name

* feat(subprojects): update wraps again

* ci(build): fix lua subproject not found

* ci(build): use awk instead of grep and sed
2023-12-26 13:16:33 +00:00
Guldoman 3ee903b16c Fix `dirmonitor` sorting issues (#1599)
* Use `PATHSEP` in path-related functions

* Don't stop on digits when getting the common part in `system.path_compare`

* Avoid sorting multiple times in `dirwatch.get_directory_files`

This also fixes the timeout detection in `recurse_pred`.
2023-12-26 13:16:33 +00:00
Guldoman 1dceaf65f5 Fix running `core.step` when receiving an event while not waiting
When `time_to_wake` was <= 0, so when a coroutine needed to be executed 
as soon as possible, we didn't check for events, so we only performed a 
`core.step` with the blink timer.
This resulted in jerky reactions to input.
2023-12-26 13:16:33 +00:00
Guldoman c4f9542509 Limit `system.{sleep,wait_event}` to timeouts >= 0 (#1666)
Otherwise we might wait forever by mistake.
2023-12-26 13:16:33 +00:00
Daniel Margarido 86b89c402d Fixed issue with set_target_size passing the wrong value to plugins (#1657)
* Fixed issue with set_target_size passing the wrong value to plugins that are split on the right and activated from the settings UI.

* Added position awareness for the all resize_child_node calls.
2023-12-26 13:16:33 +00:00
Guldoman 3972f10059 Fix deleting indentation with multiple cursors (#1670) 2023-12-26 13:16:33 +00:00
Guldoman da64a99f18 Avoid considering single spaces in `detectindent` (#1595) 2023-12-26 13:16:33 +00:00
Takase dc62c59705 refactor(build): use dmgbuild to create dmgs (#1664)
* refactor(appdmg): make dmgs with dmgbuild

* fix(appdmg.sh): typo

* refactor(appdmg.sh): don't generate config on the fly

* fix(dmgbuild): icon file

* fix(gitignore): dmgbuild settings

* chore(resources): update readme with new files

* chore(resources/macos): add missing newline
2023-12-26 13:16:33 +00:00
Takase de05ec374e feat(package): ad-hoc sign macOS bundles (#1656)
* feat(package): ad-hoc sign macOS bundles

* fix(package.sh): syntax error

* docs(readme): add instructions for self-signed builds

* docs(readme): grammar

Co-authored-by: Guldoman <giulio.lettieri@gmail.com>

---------

Co-authored-by: Guldoman <giulio.lettieri@gmail.com>
2023-12-26 13:16:33 +00:00
ThaCuber df7cf7e270 ease transparency of nagbar dim (#1658)
* ease transparency of nagbar dim

* tiny changes

* lerp alpha rather than the whole color
2023-12-26 13:16:33 +00:00
Guldoman dc3716f177 Make license time-independent (#1655) 2023-12-26 13:16:33 +00:00
Guldoman 05fbc48e03 Sanitize tab index in `Node:add_view` (#1651)
* Fix `Node:add_view` not adjusting tab index after removing `EmptyView`

* Clamp tab index in `Node:add_view`
2023-12-26 13:16:33 +00:00
Takase 6370968494 fix(dirmonitor): deadlock if error handler jumps somewhere else (#1647)
* fix: deadlock if error handler jumps somewhere else

* docs(dirmonitor): fix wrong data type in error callback

* docs(dirmonitor): clarify coroutines and deadlocks

* docs(dirmonitor): wording

Co-authored-by: Guldoman <giulio.lettieri@gmail.com>

---------

Co-authored-by: Guldoman <giulio.lettieri@gmail.com>
2023-12-26 13:16:33 +00:00
Guldoman 05e7fc4e43 Set SDL hint to prefer software render driver (#1646) 2023-12-26 13:16:33 +00:00
Takase e520227d35 ci: fix diff files having "wrong" path separator (#1648)
* ci: fix diff files having "wrong" path separator

* ci(build): use git bash to apply patches

* ci(build): fix step wording

Co-authored-by: Guldoman <giulio.lettieri@gmail.com>

---------

Co-authored-by: Guldoman <giulio.lettieri@gmail.com>
2023-12-26 13:16:33 +00:00
Guldoman 9be4583f63 Save in the `workspace` unsaved named files and `crlf` status (#1597)
* Save in the `workspace` unsaved named files

* Save in the `workspace` the `crlf` status and restore it for "new" files
2023-12-26 13:16:33 +00:00
Guldoman 7e20424b29 Ignore keypresses during IME composition (#1573)
Some IMEs continue sending keypresses even during composition, so we 
just ignore them.
2023-12-26 13:16:33 +00:00
Guldoman 9612f20685 Improve `common.serialize` (#1640)
* Make `common.serialize` more locale-independent

* Handle inf/nan numbers in `common.serialize`
2023-12-26 13:16:33 +00:00
Guldoman 1669409610 Mark unsaved named files as dirty (#1598) 2023-12-26 13:16:33 +00:00
Takase 1196bf355c fix: dim rendering when antialiasing is turned off (#1641) 2023-12-26 13:16:33 +00:00
Takase 9017fadba6 docs: fix prebuilt install instructions (#1637)
* docs: fix prebuilt install instructions

Added missing documentation for Windows and macOS.
Also updated the Linux instruction for creating desktop entries.

* docs: more clarification and grammar fixes

* docs: clarify plugin and config load in portable mode

* docs: better phrasing

Co-authored-by: Guldoman <giulio.lettieri@gmail.com>

* docs: better phrasing

Co-authored-by: Guldoman <giulio.lettieri@gmail.com>

---------

Co-authored-by: Guldoman <giulio.lettieri@gmail.com>
2023-12-26 13:16:33 +00:00
Guldoman 17cb2e86ed Remove DPI detection for default `SCALE`
This often leads to `SCALE` values that are way off, and makes Lite XL 
unusable, so we now just default it to 1.
2023-12-26 13:16:33 +00:00
Takase 4f28f718a9 docs: update invite link on README 2023-12-26 13:16:33 +00:00
Guldoman febfcb5757 Make `linewrapping` consider the expanded `Scrollbar` size
This avoids reflowing the text when hovering the scrollbar.
2023-12-26 13:16:33 +00:00
Guldoman e5c17ed3ec Fix `Scrollbar.{expanded,contracted}_size` documentation 2023-12-26 13:16:33 +00:00
Robert Hildebrandt 351ef1ecea Fixed C++14 digit separators (#1593) 2023-12-26 13:16:33 +00:00
Takase 15e05aaf03 docs(core.config): add documentation for config options (#1512)
* docs(core.config): add documentation for config options

* docs(core.config): remove wrong newline

* docs(core.config): remove trailing whitespace

* docs(core.config): add missing whitespace

* docs(core.config): add disclaimer for core.file_size_limit

* docs(core.config): fix wrong description of the pattern

* docs(core.config): fix wrong description

* docs(core.config): fix wrong description for transitions

* docs(core.config): guide user to drawwhitespace plugin

* docs(core.config): clarify libdecor usage

* docs(core.config): clarify various things

* docs(core.config): clarify more about libdecor support

* docs(core.config): fix missing enum separator

* docs(core.config): remove wayland-specific advice on config.borderless
2023-12-26 13:16:33 +00:00
sammyette 1d5f7ae9b0 feat(statusview): make a separate item for doc position percent (#1579)
* feat(statusview): make a separate item for doc position percent

* chore: remove unused variable

* fix(statusview): remove command for percent doc item

* fix(statusview): change doc percent tooltip

* fix(statusview): change percent tooltip message
2023-12-26 13:16:33 +00:00
Jefferson González 27f24701c4 Autocomplete plugin improvements (#1519)
* Add icons support to autocomplete plugin

* Removed redundant flag check

* Added support for non syntax colors

* Assert if color name not in style.syntax

* Autocomplete plugin improvements

* Support suggestion symbols scoping
  - global: all open documents
  - local: current document
  - related: all open documents with same syntax
  - none: language syntax symbols only
* Register style.syntax[] entries as icons
* Other related fixes
2023-12-26 13:16:33 +00:00
Guldoman 25a0943087 Add `NaN` guard to `View:update_scrollbar` 2023-12-26 13:16:33 +00:00
Adam b0e1469a87 Adds super as a modkey. (#1590)
* Adds super as a modkey.

* Added in super designation for windows.
2023-12-26 13:16:33 +00:00
Guldoman 2ed17dd03f Normalize strokes in fixed order (#1572)
* Use normalized strokes when removing duplicates only when appropriate

* Use normalized stroke in `keymap.unbind`

* Normalize strokes by sorting the modifiers before the keys

This also sorts the modifiers in a fixed manner, decided by 
`modkeys.keys`.
We need to do this because we display the strokes in a few places like 
the command palette.
2023-12-26 13:16:33 +00:00
Jan 3993d689fb Use Lua wrap by default (#1481)
Debian and all its derivatives ship a broken Lua 5.4 that is missing some symbols.
To work around broken distros and make development and distribution easier use the wrap by default and add an option to use the system version.
2023-12-26 13:16:33 +00:00
Takase 3f3b4d52b4 docs(core.contextmenu): add documentation for contextmenu (#1567) 2023-12-26 13:16:33 +00:00
Guldoman 9bc44e2b45 Fix returned `percent` when clicking the `Scrollbar` `track` 2023-12-26 13:16:33 +00:00
Guldoman 50102fdc3a Fix `scrollbar` misinterpreting `percent` (#1587) 2023-12-26 13:16:33 +00:00
Takase dc14860166 fix(core): defer core:open-log until everything is loaded (#1585)
* fix(core): defer core:open-log until everything is loaded

* docs(core): document why core:open-log is opened in a thread
2023-12-26 13:16:33 +00:00
Adam eb306a2ff0 Windows Installer Path Modification (#1536)
* innosetup: installation path to environment task

Also set the uninstall icon shown on add/remove programs.

* Improved path description.

---------

Co-authored-by: jgmdev <jgmdev@gmail.com>
2023-12-26 13:16:33 +00:00
Velosofy f820b9301f Add "Open with Lite XL" to windows' context menu (#1333)
Closes #423
2023-12-26 13:15:53 +00:00
66 changed files with 1614 additions and 525 deletions

View File

@ -110,6 +110,12 @@ jobs:
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
@ -117,8 +123,6 @@ jobs:
with:
name: macOS DMG Images
path: dmgs-original
- name: Install appdmg
run: cd ~; npm i appdmg; cd -
- name: Make universal bundles
run: |
bash --version
@ -200,13 +204,14 @@ jobs:
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/lua-5.4.4" >> $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: |
# Download the subprojects first so we can patch it before configuring.
# This avoids reconfiguring the subprojects when compiling.
meson subprojects download
Get-Content -Path resources/windows/001-lua-unicode.diff -Raw | patch -d $env:LUA_SUBPROJECT_PATH -p1 --forward
meson setup --wrap-mode=forcefallback build
- name: Build
run: |

View File

@ -185,8 +185,8 @@ jobs:
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install appdmg
run: cd ~; npm i appdmg; cd -
- name: Install dmgbuild
run: pip install dmgbuild
- name: Prepare DMG Images
run: |
mkdir -p dmgs-addons dmgs-normal

1
.gitignore vendored
View File

@ -22,3 +22,4 @@ LiteXL*
!resources/windows/*.diff
!resources/windows/*.exe.manifest.in
!resources/macos/*.py

View File

@ -1,4 +1,4 @@
Copyright (c) 2020-2021 Lite XL Team
Copyright (c) 2020-present Lite XL Team
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in

View File

@ -1,7 +1,7 @@
# Lite XL
[![CI]](https://github.com/lite-xl/lite-xl/actions/workflows/build.yml)
[![Discord Badge Image]](https://discord.gg/RWzqC3nx7K)
[![Discord Badge Image]](https://discord.gg/UQKnzBhY5H)
![screenshot-dark]
@ -81,6 +81,39 @@ affects only the place where the application is actually installed.
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:
@ -91,6 +124,7 @@ cd lite-xl
```
To run lite-xl without installing:
```sh
./lite-xl
```
@ -103,21 +137,59 @@ 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/
```
#### Add Lite XL to PATH
To run Lite XL from the command line, you must add it to PATH.
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:
Alternatively on recent versions of GNOME and KDE Plasma,
you can add `$HOME/.local/bin` to PATH via `~/.config/environment.d/envvars.conf`:
```ini
PATH=$HOME/.local/bin:$PATH
```
> **Note**
> Some systems might not load `.bashrc` when logging in.
> This can cause problems with launching applications from the desktop / menu.
#### Add Lite XL to application launchers
To get the icon to show up in app launcher, you need to create a desktop
entry and put it into `/usr/share/applications` or `~/.local/share/applications`.
Here is an example for a desktop entry in `~/.local/share/applications/com.lite_xl.LiteXL.desktop`,
assuming Lite XL is in PATH:
```ini
[Desktop Entry]
Type=Application
Name=Lite XL
Comment=A lightweight text editor written in Lua
Exec=lite-xl %F
Icon=lite-xl
Terminal=false
StartupWMClass=lite-xl
Categories=Development;IDE;
MimeType=text/plain;inode/directory;
```
To get the icon to show up in app launcher immediately, run:
```sh
xdg-desktop-menu forceupdate
```
You may need to logout and login again to see icon in app launcher.
Alternatively, you may log out and log in again.
To uninstall just run:
#### Uninstall
To uninstall Lite XL, run:
```sh
rm -f $HOME/.local/bin/lite-xl
@ -127,7 +199,6 @@ rm -rf $HOME/.local/share/icons/hicolor/scalable/apps/lite-xl.svg \
$HOME/.local/share/lite-xl
```
## Contributing
Any additional functionality that can be added through a plugin should be done

View File

@ -38,7 +38,7 @@ show_help() {
echo "-v --version VERSION Sets the version on the package name."
echo "-A --appimage Create an AppImage (Linux only)."
echo "-D --dmg Create a DMG disk image (macOS only)."
echo " Requires NPM and AppDMG."
echo " 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,"

View File

@ -43,7 +43,7 @@ local function save(filename)
core.log("Saved \"%s\"", saved_filename)
else
core.error(err)
core.nag_view:show("Saving failed", string.format("Could not save \"%s\" do you want to save to another location?", doc().filename), {
core.nag_view:show("Saving failed", string.format("Couldn't save file \"%s\". Do you want to save to another location?", doc().filename), {
{ text = "No", default_no = true },
{ text = "Yes", default_yes = true }
}, function(item)
@ -340,10 +340,11 @@ local commands = {
local text = dv.doc:get_text(line1, 1, line1, col1)
if #text >= indent_size and text:find("^ *$") then
dv.doc:delete_to_cursor(idx, 0, -indent_size)
return
goto continue
end
end
dv.doc:delete_to_cursor(idx, translate.previous_char)
::continue::
end
end,
@ -544,6 +545,11 @@ local commands = {
dv.doc.crlf = not dv.doc.crlf
end,
["doc:toggle-overwrite"] = function(dv)
dv.doc.overwrite = not dv.doc.overwrite
core.blink_reset() -- to show the cursor has changed edit modes
end,
["doc:save-as"] = function(dv)
local last_doc = core.last_active_view and core.last_active_view.doc
local text

View File

@ -164,13 +164,16 @@ local function is_in_any_selection(line, col)
end
local function select_add_next(all)
local il1, ic1 = doc():get_selection(true)
for idx, l1, c1, l2, c2 in doc():get_selections(true, true) do
local il1, ic1
for _, l1, c1, l2, c2 in doc():get_selections(true, true) do
if not il1 then
il1, ic1 = l1, c1
end
local text = doc():get_text(l1, c1, l2, c2)
repeat
l1, c1, l2, c2 = search.find(doc(), l2, c2, text, { wrap = true })
if l1 == il1 and c1 == ic1 then break end
if l2 and (all or not is_in_any_selection(l2, c2)) then
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)
@ -266,7 +269,7 @@ command.add(valid_for_finding, {
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, sc2, last_text, case_sensitive, find_regex, false)
local line1, col1, line2, col2 = last_fn(dv.doc, sl2, sc2, last_text, case_sensitive, find_regex, false)
if line1 then
dv.doc:set_selection(line2, col2, line1, col1)
dv:scroll_to_line(line2, true)

View File

@ -226,7 +226,7 @@ function common.path_suggest(text, root)
if root and root:sub(-1) ~= PATHSEP then
root = root .. PATHSEP
end
local path, name = text:match("^(.-)([^/\\]*)$")
local path, name = text:match("^(.-)([^"..PATHSEP.."]*)$")
local clean_dotslash = false
-- ignore root if path is absolute
local is_absolute = common.is_absolute_path(text)
@ -279,7 +279,7 @@ end
---@param text string The input path.
---@return string[]
function common.dir_path_suggest(text)
local path, name = text:match("^(.-)([^/\\]*)$")
local path, name = text:match("^(.-)([^"..PATHSEP.."]*)$")
local files = system.list_dir(path == "" and "." or path) or {}
local res = {}
for _, file in ipairs(files) do
@ -298,7 +298,7 @@ end
---@param dir_list string[] A list of paths to filter.
---@return string[]
function common.dir_list_suggest(text, dir_list)
local path, name = text:match("^(.-)([^/\\]*)$")
local path, name = text:match("^(.-)([^"..PATHSEP.."]*)$")
local res = {}
for _, dir_path in ipairs(dir_list) do
if dir_path:lower():find(text:lower(), nil, true) == 1 then
@ -378,12 +378,15 @@ 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 ""
if type(val) == "string" then
local ty = type(val)
if ty == "string" then
local out = string.format("%q", val)
if escape then
out = string.gsub(out, "\\\n", "\\n")
@ -395,7 +398,7 @@ local function serialize(val, pretty, indent_str, escape, sort, limit, level)
out = string.gsub(out, "\\13", "\\r")
end
return out
elseif type(val) == "table" then
elseif ty == "table" then
-- early exit
if level >= limit then return tostring(val) end
local next_indent = pretty and (indent .. indent_str) or ""
@ -410,6 +413,12 @@ local function serialize(val, pretty, indent_str, escape, sort, limit, level)
if sort then table.sort(t) end
return "{" .. newline .. table.concat(t, "," .. newline) .. newline .. indent .. "}"
end
if ty == "number" then
-- tostring is locale-dependent, so we need to replace an eventual `,` with `.`
local res, _ = tostring(val):gsub(",", ".")
-- handle inf/nan
return oddvals[res] or res
end
return tostring(val)
end
@ -452,7 +461,7 @@ end
function common.basename(path)
-- a path should never end by / or \ except if it is '/' (unix root) or
-- 'X:\' (windows drive)
return path:match("[^\\/]+$") or path
return path:match("[^"..PATHSEP.."]+$") or path
end
@ -461,7 +470,7 @@ end
---@param path string
---@return string|nil
function common.dirname(path)
return path:match("(.+)[\\/][^\\/]+$")
return path:match("(.+)["..PATHSEP.."][^"..PATHSEP.."]+$")
end
@ -504,10 +513,10 @@ end
local function split_on_slash(s, sep_pattern)
local t = {}
if s:match("^[/\\]") then
if s:match("^["..PATHSEP.."]") then
t[#t + 1] = ""
end
for fragment in string.gmatch(s, "([^/\\]+)") do
for fragment in string.gmatch(s, "([^"..PATHSEP.."]+)") do
t[#t + 1] = fragment
end
return t
@ -640,7 +649,7 @@ 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("(.*)[/\\](.+)$")
local updir, basedir = path:match("(.*)["..PATHSEP.."](.+)$")
table.insert(subdirs, 1, basedir or path)
path = updir
end

View File

@ -2,15 +2,71 @@ local common = require "core.common"
local config = {}
---The frame rate of Lite XL.
---Note that setting this value to the screen's refresh rate
---does not eliminate screen tearing.
---
---Defaults to 60.
---@type number
config.fps = 60
---Maximum number of log items that will be stored.
---When the number of log items exceed this value, old items will be discarded.
---
---Defaults to 800.
---@type number
config.max_log_items = 800
---The timeout, in seconds, before a message dissapears from StatusView.
---
---Defaults to 5.
---@type number
config.message_timeout = 5
---The number of pixels scrolled per-step.
---
---Defaults to 50 * SCALE.
---@type number
config.mouse_wheel_scroll = 50 * SCALE
---Enables/disables transitions when scrolling with the scrollbar.
---When enabled, the scrollbar will have inertia and slowly move towards the cursor.
---Otherwise, the scrollbar will immediately follow the cursor.
---
---Defaults to false.
---@type boolean
config.animate_drag_scroll = false
---Enables/disables scrolling past the end of a document.
---
---Defaults to true.
---@type boolean
config.scroll_past_end = true
---@type "expanded" | "contracted" | false @Force the scrollbar status of the DocView
---@alias config.scrollbartype
---| "expanded" # A thicker scrollbar is shown at all times.
---| "contracted" # A thinner scrollbar is shown at all times.
---| false # The scrollbar expands when the cursor hovers over it.
---Controls whether the DocView scrollbar is always shown or hidden.
---This option does not affect other View's scrollbars.
---
---Defaults to false.
---@type config.scrollbartype
config.force_scrollbar_status = false
---The file size limit, in megabytes.
---Files larger than this size will not be shown in the file picker.
---
---Defaults to 10.
---@type number
config.file_size_limit = 10
---A list of files and directories to ignore.
---Each element is a Lua pattern, where patterns ending with a forward slash
---are recognized as directories while patterns ending with an anchor ("$") are
---recognized as files.
---@type string[]
config.ignore_files = {
-- folders
"^%.svn/", "^%.git/", "^%.hg/", "^CVS/", "^%.Trash/", "^%.Trash%-.*/",
@ -21,46 +77,194 @@ config.ignore_files = {
"%.suo$", "%.pdb$", "%.idb$", "%.class$", "%.psd$", "%.db$",
"^desktop%.ini$", "^%.DS_Store$", "^%.directory$",
}
---Lua pattern used to find symbols when advanced syntax highlighting
---is not available.
---This pattern is also used for navigation, e.g. move to next word.
---
---The default pattern matches all letters, followed by any number
---of letters and digits.
---@type string
config.symbol_pattern = "[%a_][%w_]*"
---A list of characters that delimits a word.
---
---The default is ``" \t\n/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-"``
---@type string
config.non_word_chars = " \t\n/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-"
---The timeout, in seconds, before several consecutive actions
---are merged as a single undo step.
---
---The default is 0.3 seconds.
---@type number
config.undo_merge_timeout = 0.3
---The maximum number of undo steps per-document.
---
---The default is 10000.
---@type number
config.max_undos = 10000
---The maximum number of tabs shown at a time.
---
---The default is 8.
---@type number
config.max_tabs = 8
---Shows/hides the tab bar when there is only one tab open.
---
---The tab bar is always shown by default.
---@type boolean
config.always_show_tabs = true
-- Possible values: false, true, "no_selection"
---@alias config.highlightlinetype
---| true # Always highlight the current line.
---| false # Never highlight the current line.
---| "no_selection" # Highlight the current line if no text is selected.
---Highlights the current line.
---
---The default is true.
---@type config.highlightlinetype
config.highlight_current_line = true
---The spacing between each line of text.
---
---The default is 120% of the height of the text (1.2).
---@type number
config.line_height = 1.2
---The number of spaces each level of indentation represents.
---
---The default is 2.
---@type number
config.indent_size = 2
---The type of indentation.
---
---The default is "soft" (spaces).
---@type "soft" | "hard"
config.tab_type = "soft"
---Do not remove whitespaces when advancing to the next line.
---
---Defaults to false.
---@type boolean
config.keep_newline_whitespace = false
---Default line endings for new files.
---
---Defaults to `crlf` (`\r\n`) on Windows and `lf` (`\n`) on everything else.
---@type "crlf" | "lf"
config.line_endings = PLATFORM == "Windows" and "crlf" or "lf"
---Maximum number of characters per-line for the line guide.
---
---Defaults to 80.
---@type number
config.line_limit = 80
---Maximum number of project files to keep track of.
---If the number of files in the project exceeds this number,
---Lite XL will not be able to keep track of them.
---They will be not be searched when searching for files or text.
---
---Defaults to 2000.
---@type number
config.max_project_files = 2000
---Enables/disables all transitions.
---
---Defaults to true.
---@type boolean
config.transitions = true
---Enable/disable individual transitions.
---These values are overriden by `config.transitions`.
config.disabled_transitions = {
---Disables scrolling transitions.
scroll = false,
---Disables transitions for CommandView's suggestions list.
commandview = false,
---Disables transitions for showing/hiding the context menu.
contextmenu = false,
---Disables transitions when clicking on log items in LogView.
logview = false,
---Disables transitions for showing/hiding the Nagbar.
nagbar = false,
---Disables transitions when scrolling the tab bar.
tabs = false,
---Disables transitions when a tab is being dragged.
tab_drag = false,
---Disables transitions when a notification is shown.
statusbar = false,
}
---The rate of all transitions.
---
---Defaults to 1.
---@type number
config.animation_rate = 1.0
---The caret's blinking period, in seconds.
---
---Defaults to 0.8.
---@type number
config.blink_period = 0.8
---Disables caret blinking.
---
---Defaults to false.
---@type boolean
config.disable_blink = false
---Draws whitespaces as dots.
---This option is deprecated.
---Please use the drawwhitespace plugin instead.
---@deprecated
config.draw_whitespace = false
---Disables system-drawn window borders.
---
---When set to true, Lite XL draws its own window decorations,
---which can be useful for certain setups.
---
---Defaults to false.
---@type boolean
config.borderless = false
---Shows/hides the close buttons on tabs.
---When hidden, users can close tabs via keyboard shortcuts or commands.
---
---Defaults to true.
---@type boolean
config.tab_close_button = true
---Maximum number of clicks recognized by Lite XL.
---
---Defaults to 3.
---@type number
config.max_clicks = 3
-- set as true to be able to test non supported plugins
---Disables plugin version checking.
---Do not change this unless you know what you are doing.
---
---Defaults to false.
---@type boolean
config.skip_plugins_version = false
-- holds the plugins real config table
local plugins_config = {}
-- virtual representation of plugins config table
---A table containing configuration for all the plugins.
---
---This is a metatable that automaticaly creates a minimal
---configuration when a plugin is initially configured.
---Each plugins will then call `common.merge()` to get the finalized
---plugin config.
---Do not use raw operations on this table.
---@type table
config.plugins = {}
-- allows virtual access to the plugins config table

View File

@ -12,11 +12,31 @@ 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
@ -55,12 +75,19 @@ local function update_items_size(items, update_binding)
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 }
@ -94,6 +121,7 @@ function ContextMenu:show(x, y)
return false
end
---Hides the context menu.
function ContextMenu:hide()
self.show_context_menu = false
self.items = nil
@ -102,6 +130,8 @@ function ContextMenu:hide()
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
@ -115,8 +145,12 @@ 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 end
if not self.show_context_menu then return false end
self.selected = -1
for i, item, x, y, w, h in self:each_item() do
@ -128,6 +162,8 @@ function ContextMenu:on_mouse_moved(px, py)
return true
end
---Event handler for when the selection is confirmed.
---@param item core.contextmenu.item
function ContextMenu:on_selected(item)
if type(item.command) == "string" then
command.perform(item.command)
@ -140,6 +176,7 @@ 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
@ -147,6 +184,7 @@ function ContextMenu:focus_previous()
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
@ -154,10 +192,13 @@ function ContextMenu:focus_next()
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()
@ -166,6 +207,12 @@ function ContextMenu:call_selected_item()
end
end
---Event handler for mouse press.
---@param button core.view.mousebutton
---@param px number
---@param py number
---@param clicks number
---@return boolean # true if the event is caught.
function ContextMenu:on_mouse_pressed(button, px, py, clicks)
local caught = false
@ -186,14 +233,20 @@ function ContextMenu:on_mouse_pressed(button, px, py, clicks)
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")
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
@ -206,6 +259,7 @@ function ContextMenu:draw()
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

View File

@ -91,6 +91,7 @@ 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
@ -102,7 +103,10 @@ function dirwatch:check(change_callback, scan_time, wait_time)
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
@ -186,47 +190,45 @@ end
-- "root" will by an absolute path without trailing '/'
-- "path" will be a path starting without '/' and without trailing '/'
-- or the empty string.
-- It will identifies a sub-path within "root.
-- 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.
-- When recursing, "root" will always be the same, only "path" will change.
-- Returns a list of file "items". In each item the "filename" will be the
-- complete file path relative to "root" *without* the trailing '/', and without the starting '/'.
function dirwatch.get_directory_files(dir, root, path, t, entries_count, recurse_pred)
function dirwatch.get_directory_files(dir, root, path, entries_count, recurse_pred)
local t = {}
local t0 = system.get_time()
local t_elapsed = system.get_time() - t0
local dirs, files = {}, {}
local ignore_compiled = compile_ignore_files()
local all = system.list_dir(root .. PATHSEP .. path)
if not all then return nil end
for _, file in ipairs(all or {}) do
local 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(info.type == "dir" and dirs or files, info)
entries_count = entries_count + 1
table.insert(entries, info)
end
end
table.sort(entries, compare_file)
local recurse_complete = true
table.sort(dirs, compare_file)
for _, f in ipairs(dirs) do
table.insert(t, f)
if recurse_pred(dir, f.filename, entries_count, t_elapsed) then
local _, complete, n = dirwatch.get_directory_files(dir, root, f.filename, t, entries_count, recurse_pred)
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
table.sort(files, compare_file)
for _, f in ipairs(files) do
table.insert(t, f)
end
return t, recurse_complete, entries_count

View File

@ -1,5 +1,6 @@
local Object = require "core.object"
local Highlighter = require "core.doc.highlighter"
local translate = require "core.doc.translate"
local core = require "core"
local syntax = require "core.syntax"
local config = require "core.config"
@ -27,8 +28,10 @@ function Doc:new(filename, abs_filename, new_file)
self:load(filename)
end
end
if new_file then
self.crlf = config.line_endings == "crlf"
end
end
function Doc:reset()
self.lines = { "\n" }
@ -38,10 +41,10 @@ function Doc:reset()
self.redo_stack = { idx = 1 }
self.clean_change_id = 1
self.highlighter = Highlighter(self)
self.overwrite = false
self:reset_syntax()
end
function Doc:reset_syntax()
local header = self:get_text(1, 1, self:position_offset(1, 1, 128))
local path = self.abs_filename
@ -56,14 +59,12 @@ function Doc:reset_syntax()
end
end
function Doc:set_filename(filename, abs_filename)
self.filename = filename
self.abs_filename = abs_filename
self:reset_syntax()
end
function Doc:load(filename)
local fp = assert(io.open(filename, "rb"))
self:reset()
@ -85,7 +86,6 @@ function Doc:load(filename)
self:reset_syntax()
end
function Doc:reload()
if self.filename then
local sel = { self:get_selection() }
@ -95,7 +95,6 @@ function Doc:reload()
end
end
function Doc:save(filename, abs_filename)
if not filename then
assert(self.filename, "no filename set to default to")
@ -115,26 +114,23 @@ function Doc:save(filename, abs_filename)
self:clean()
end
function Doc:get_name()
return self.filename or "unsaved"
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
end
function Doc:clean()
self.clean_change_id = self:get_change_id()
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,
@ -142,7 +138,6 @@ function Doc:get_indent_info()
self.indent_info.confirmed
end
function Doc:get_change_id()
return self.undo_stack.idx
end
@ -166,13 +161,14 @@ function Doc:get_selection(sort)
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]
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
@ -232,7 +228,6 @@ function Doc:add_selection(line1, col1, line2, col2, swap)
self.last_selection = target
end
function Doc:remove_selection(idx)
if self.last_selection >= idx then
self.last_selection = self.last_selection - 1
@ -240,7 +235,6 @@ function Doc:remove_selection(idx)
common.splice(self.selections, (idx - 1) * 4 + 1, 4)
end
function Doc:set_selection(line1, col1, line2, col2, swap)
self.selections = {}
self:set_selections(1, line1, col1, line2, col2, swap)
@ -278,6 +272,7 @@ function Doc:get_selections(sort_intra, idx_reverse)
return selection_iterator, { self.selections, sort_intra, idx_reverse },
idx_reverse == true and ((#self.selections / 4) + 1) or ((idx_reverse or -1) + 1)
end
-- End of cursor seciton.
function Doc:sanitize_position(line, col)
@ -290,7 +285,6 @@ function Doc:sanitize_position(line, col)
return line, common.clamp(col, 1, #self.lines[line])
end
local function position_offset_func(self, line, col, fn, ...)
line, col = self:sanitize_position(line, col)
return fn(self, line, col, ...)
@ -329,7 +323,6 @@ function Doc:position_offset(line, col, ...)
end
end
function Doc:get_text(line1, col1, line2, col2)
line1, col1 = self:sanitize_position(line1, col1)
line2, col2 = self:sanitize_position(line2, col2)
@ -345,13 +338,11 @@ function Doc:get_text(line1, col1, line2, col2)
return table.concat(lines)
end
function Doc:get_char(line, col)
line, col = self:sanitize_position(line, col)
return self.lines[line]:sub(col, col)
end
local function push_undo(undo_stack, time, type, ...)
undo_stack[undo_stack.idx] = { type = type, time = time, ... }
undo_stack[undo_stack.idx - config.max_undos] = nil
@ -412,7 +403,8 @@ function Doc:raw_insert(line, col, text, undo_stack, time)
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)
self:set_selections(idx, cline1 + line_addition, ccol1 + column_addition, cline2 + line_addition,
ccol2 + column_addition)
end
-- push undo
@ -425,7 +417,6 @@ function Doc:raw_insert(line, col, text, undo_stack, time)
self:sanitize_selection()
end
function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time)
-- push undo
local text = self:get_text(line1, col1, line2, col2)
@ -484,15 +475,17 @@ function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time)
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")
end
function Doc:remove(line1, col1, line2, col2)
self.redo_stack = { idx = 1 }
line1, col1 = self:sanitize_position(line1, col1)
@ -502,28 +495,34 @@ function Doc:remove(line1, col1, line2, col2)
self:on_text_change("remove")
end
function Doc:undo()
pop_undo(self, self.undo_stack, self.redo_stack, false)
end
function Doc:redo()
pop_undo(self, self.redo_stack, self.undo_stack, false)
end
function Doc:text_input(text, idx)
for sidx, line1, col1, line2, col2 in self:get_selections(true, idx or true) do
local had_selection = false
if line1 ~= line2 or col1 ~= col2 then
self:delete_to_cursor(sidx)
had_selection = true
end
if self.overwrite
and not had_selection
and col1 < #self.lines[line1]
and text:ulen() == 1 then
self:remove(line1, col1, translate.next_char(self, line1, col1))
end
self:insert(line1, col1, text)
self:move_to_cursor(sidx, #text)
end
end
function Doc:ime_text_editing(text, start, length, idx)
for sidx, line1, col1, line2, col2 in self:get_selections(true, idx or true) do
if line1 ~= line2 or col1 ~= col2 then
@ -534,7 +533,6 @@ function Doc:ime_text_editing(text, start, length, idx)
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)
@ -564,7 +562,6 @@ function Doc:replace(fn)
return results
end
function Doc:delete_to_cursor(idx, ...)
for sidx, line1, col1, line2, col2 in self:get_selections(true, idx) do
if line1 ~= line2 or col1 ~= col2 then
@ -578,6 +575,7 @@ function Doc:delete_to_cursor(idx, ...)
end
self:merge_cursors(idx)
end
function Doc:delete_to(...) return self:delete_to_cursor(nil, ...) end
function Doc:move_to_cursor(idx, ...)
@ -586,8 +584,8 @@ function Doc:move_to_cursor(idx, ...)
end
self:merge_cursors(idx)
end
function Doc:move_to(...) return self:move_to_cursor(nil, ...) end
function Doc:move_to(...) return self:move_to_cursor(nil, ...) end
function Doc:select_to_cursor(idx, ...)
for sidx, line, col, line2, col2 in self:get_selections(false, idx) do
@ -596,8 +594,8 @@ function Doc:select_to_cursor(idx, ...)
end
self:merge_cursors(idx)
end
function Doc:select_to(...) return self:select_to_cursor(nil, ...) end
function Doc:select_to(...) return self:select_to_cursor(nil, ...) end
function Doc:get_indent_string()
local indent_type, indent_size = self:get_indent_info()
@ -669,5 +667,4 @@ function Doc:on_close()
core.log_quiet("Closed doc \"%s\"", self:get_name())
end
return Doc

View File

@ -66,7 +66,18 @@ function search.find(doc, line, col, text, opt)
s, e = search_func(line_text, pattern, col, plain)
end
if s then
return line, s, line, e + 1
local line2 = line
-- If we've matched the newline too,
-- return until the initial character of the next line.
if e >= #doc.lines[line] then
line2 = line + 1
e = 0
end
-- 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
end
end
col = opt.reverse and -1 or 1
end

View File

@ -460,6 +460,13 @@ function DocView:draw_line_text(line, x, y)
return self:get_line_height()
end
function DocView:draw_overwrite_caret(x, y, width)
local lh = self:get_line_height()
renderer.draw_rect(x, y + lh - style.caret_width, width, style.caret_width, style.caret)
end
function DocView:draw_caret(x, y)
local lh = self:get_line_height()
renderer.draw_rect(x, y, style.caret_width, lh, style.caret)
@ -559,7 +566,12 @@ function DocView:draw_overlay()
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))
local x, y = self:get_line_screen_position(line1, col1)
if self.doc.overwrite then
self:draw_overwrite_caret(x, y, self:get_font():get_width(self.doc:get_char(line1, col1)))
else
self:draw_caret(x, y)
end
end
end
end

View File

@ -102,7 +102,7 @@ local function strip_leading_path(filename)
end
local function strip_trailing_slash(filename)
if filename:match("[^:][/\\]$") then
if filename:match("[^:]["..PATHSEP.."]$") then
return filename:sub(1, -2)
end
return filename
@ -120,9 +120,7 @@ local function show_max_files_warning(dir)
"Too many files in project directory: stopped reading at "..
config.max_project_files.." files. For more information see "..
"usage.md at https://github.com/lite-xl/lite-xl."
if core.status_view then
core.status_view:show_message("!", style.accent, message)
end
core.warn(message)
end
@ -184,7 +182,7 @@ local function refresh_directory(topdir, target)
directory_start_idx = directory_start_idx + 1
end
local files = dirwatch.get_directory_files(topdir, topdir.name, (target or ""), {}, 0, function() return false end)
local files = dirwatch.get_directory_files(topdir, topdir.name, (target or ""), 0, function() return false end)
local change = false
-- If this file doesn't exist, we should be calling this on our parent directory, assume we'll do that.
@ -265,7 +263,7 @@ function core.add_project_directory(path)
local fstype = PLATFORM == "Linux" and system.get_fs_type(topdir.name) or "unknown"
topdir.force_scans = (fstype == "nfs" or fstype == "fuse")
local t, complete, entries_count = dirwatch.get_directory_files(topdir, topdir.name, "", {}, 0, timed_max_files_pred)
local t, complete, entries_count = dirwatch.get_directory_files(topdir, topdir.name, "", 0, timed_max_files_pred)
topdir.files = t
if not complete then
topdir.slow_filesystem = not complete and (entries_count <= config.max_project_files)
@ -810,7 +808,11 @@ function core.init()
end
if not plugins_success or got_user_error or got_project_error then
-- defer LogView to after everything is initialized,
-- so that EmptyView won't be added after LogView.
core.add_thread(function()
command.perform("core:open-log")
end)
end
core.configure_borderless_window()
@ -1274,6 +1276,9 @@ function core.on_event(type, ...)
elseif type == "textediting" then
ime.on_text_editing(...)
elseif type == "keypressed" then
-- In some cases during IME composition input is still sent to us
-- so we just ignore it.
if ime.editing then return false end
did_keymap = keymap.on_key_pressed(...)
elseif type == "keyreleased" then
keymap.on_key_released(...)
@ -1418,11 +1423,11 @@ local run_threads = coroutine.wrap(function()
-- stop running threads if we're about to hit the end of frame
if system.get_time() - core.frame_start > max_time then
coroutine.yield(0)
coroutine.yield(0, false)
end
end
coroutine.yield(minimal_time_to_wake)
coroutine.yield(minimal_time_to_wake, true)
end
end)
@ -1430,10 +1435,15 @@ end)
function core.run()
local next_step
local last_frame_time
local run_threads_full = 0
while true do
core.frame_start = system.get_time()
local time_to_wake = run_threads()
local time_to_wake, threads_done = run_threads()
if threads_done then
run_threads_full = run_threads_full + 1
end
local did_redraw = false
local did_step = false
local force_draw = core.redraw and last_frame_time and core.frame_start - last_frame_time > (1 / config.fps)
if force_draw or not next_step or system.get_time() >= next_step then
if core.step() then
@ -1441,11 +1451,12 @@ function core.run()
last_frame_time = core.frame_start
end
next_step = nil
did_step = true
end
if core.restart_request or core.quit_request then break end
if not did_redraw then
if system.window_has_focus() then
if system.window_has_focus() or not did_step or run_threads_full < 2 then
local now = system.get_time()
if not next_step then -- compute the time until the next blink
local t = now - core.blink_start
@ -1454,7 +1465,7 @@ function core.run()
local cursor_time_to_wake = dt + 1 / config.fps
next_step = now + cursor_time_to_wake
end
if time_to_wake > 0 and system.wait_event(math.min(next_step - now, time_to_wake)) then
if system.wait_event(math.min(next_step - now, time_to_wake)) then
next_step = nil -- if we've recevied an event, perform a step
end
else
@ -1462,6 +1473,7 @@ function core.run()
next_step = nil -- perform a step when we're not in focus if get we an event
end
else -- if we redrew, then make sure we only draw at most FPS/sec
run_threads_full = 0
local now = system.get_time()
local elapsed = now - core.frame_start
local next_frame = math.max(0, 1 / config.fps - elapsed)

View File

@ -40,15 +40,19 @@ local modkeys = modkeys_os.keys
---@return string
local function normalize_stroke(stroke)
local stroke_table = {}
for modkey in stroke:gmatch("(%w+)%+") do
table.insert(stroke_table, modkey)
for key in stroke:gmatch("[^+]+") do
table.insert(stroke_table, key)
end
if not next(stroke_table) then
return stroke
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
table.sort(stroke_table)
local new_stroke = table.concat(stroke_table, "+") .. "+"
return new_stroke .. stroke:sub(new_stroke:len() + 1)
end
return a < b
end)
return table.concat(stroke_table, "+")
end
@ -56,15 +60,16 @@ end
---@param key string
---@return string
local function key_to_stroke(key)
local stroke = ""
local keys = { key }
for _, mk in ipairs(modkeys) do
if keymap.modkeys[mk] then
stroke = stroke .. mk .. "+"
table.insert(keys, mk)
end
end
return normalize_stroke(stroke) .. key
return normalize_stroke(table.concat(keys, "+"))
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
@ -90,12 +95,12 @@ end
---@param map keymap.map
local function remove_duplicates(map)
for stroke, commands in pairs(map) do
stroke = normalize_stroke(stroke)
local normalized_stroke = normalize_stroke(stroke)
if type(commands) == "string" or type(commands) == "function" then
commands = { commands }
end
if keymap.map[stroke] then
for _, registered_cmd in ipairs(keymap.map[stroke]) do
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
@ -172,7 +177,8 @@ end
---@param shortcut string
---@param cmd string
function keymap.unbind(shortcut, cmd)
remove_only(keymap.map, normalize_stroke(shortcut), cmd)
shortcut = normalize_stroke(shortcut)
remove_only(keymap.map, shortcut, cmd)
remove_only(keymap.reverse_map, cmd, shortcut)
end
@ -197,10 +203,6 @@ end
-- Events listening
--------------------------------------------------------------------------------
function keymap.on_key_pressed(k, ...)
-- In MacOS and Windows during IME composition input is still sent to us
-- so we just ignore it
if PLATFORM ~= "Linux" and ime.editing then return false end
local mk = modkey_map[k]
if mk then
keymap.modkeys[mk] = true
@ -339,6 +341,7 @@ keymap.add_direct {
["ctrl+x"] = "doc:cut",
["ctrl+c"] = "doc:copy",
["ctrl+v"] = "doc:paste",
["insert"] = "doc:toggle-overwrite",
["ctrl+insert"] = "doc:copy",
["shift+insert"] = "doc:paste",
["escape"] = { "command:escape", "doc:select-none", "dialog:select-no" },

View File

@ -7,8 +7,12 @@ modkeys.map = {
["right shift"] = "shift",
["left alt"] = "alt",
["right alt"] = "altgr",
["left gui"] = "super",
["left windows"] = "super",
["right gui"] = "super",
["right windows"] = "super"
}
modkeys.keys = { "ctrl", "alt", "altgr", "shift" }
modkeys.keys = { "ctrl", "shift", "alt", "altgr", "super" }
return modkeys

View File

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

View File

@ -24,6 +24,7 @@ function NagView:new()
self.scrollable = true
self.target_height = 0
self.on_mouse_pressed_root = nil
self.dim_alpha = 0
end
function NagView:get_title()
@ -68,7 +69,9 @@ function NagView:dim_window_content()
oy = oy + self.show_height
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)
local dim_color = { table.unpack(style.nagbar_dim) }
dim_color[4] = style.nagbar_dim[4] * self.dim_alpha
renderer.draw_rect(ox, oy, w, h, dim_color)
end)
end
@ -172,10 +175,13 @@ function NagView:update()
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")
local target_height = self:get_target_height()
self:move_towards(self, "show_height", target_height, nil, "nagbar")
self:move_towards(self, "underline_progress", 1, nil, "nagbar")
self:move_towards(self, "dim_alpha", self.show_height / target_height, nil, "nagbar")
else
self:move_towards(self, "show_height", 0, nil, "nagbar")
self:move_towards(self, "dim_alpha", 0, nil, "nagbar")
if self.show_height <= 0 then
self.title = nil
self.message = nil

View File

@ -177,8 +177,12 @@ function Node:add_view(view, idx)
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)
if idx and idx > 1 then
idx = idx - 1
end
table.insert(self.views, idx or (#self.views + 1), view)
end
idx = common.clamp(idx or (#self.views + 1), 1, (#self.views + 1))
table.insert(self.views, idx, view)
self:set_active_view(view)
end

View File

@ -313,10 +313,10 @@ function RootView:on_mouse_moved(x, y, dx, dy)
if self.dragged_divider then
local node = self.dragged_divider
if node.type == "hsplit" then
x = common.clamp(x, 0, self.root_node.size.x * 0.95)
x = common.clamp(x - node.position.x, 0, self.root_node.size.x * 0.95)
resize_child_node(node, "x", x, dx)
elseif node.type == "vsplit" then
y = common.clamp(y, 0, self.root_node.size.y * 0.95)
y = common.clamp(y - node.position.y, 0, self.root_node.size.y * 0.95)
resize_child_node(node, "y", y, dy)
end
node.divider = common.clamp(node.divider, 0.01, 0.99)
@ -406,10 +406,10 @@ function RootView:on_touch_moved(x, y, dx, dy, ...)
if self.dragged_divider then
local node = self.dragged_divider
if node.type == "hsplit" then
x = common.clamp(x, 0, self.root_node.size.x * 0.95)
x = common.clamp(x - node.position.x, 0, self.root_node.size.x * 0.95)
resize_child_node(node, "x", x, dx)
elseif node.type == "vsplit" then
y = common.clamp(y, 0, self.root_node.size.y * 0.95)
y = common.clamp(y - node.position.y, 0, self.root_node.size.y * 0.95)
resize_child_node(node, "y", y, dy)
end
node.divider = common.clamp(node.divider, 0.01, 0.99)

View File

@ -58,9 +58,9 @@ function Scrollbar:new(options)
---@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.contracted_size = options.contracted_size
---@type number? @Override the default value specified by `style.expanded_scrollbar_size`
self.expanded_size = options.expanded_size
end
@ -121,7 +121,7 @@ function Scrollbar:_get_thumb_rect_normal()
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),
nr.along + self.percent * (nr.along_size - along_size),
across_size,
along_size
end
@ -189,8 +189,9 @@ function Scrollbar:_on_mouse_pressed_normal(button, x, y, clicks)
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 (y - self.normal_rect.along - along_size / 2) / self.normal_rect.along_size
return common.clamp((y - nr.along - along_size / 2) / (nr.along_size - along_size), 0, 1)
end
end
end
@ -237,7 +238,8 @@ 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)
local _, _, _, along_size = self:_get_thumb_rect_normal()
return common.clamp((y - nr.along + self.drag_start_offset) / (nr.along_size - along_size), 0, 1)
end
return self:_update_hover_status_normal(x, y)
end
@ -280,7 +282,7 @@ function Scrollbar:set_size(x, y, w, h, scrollable)
end
---Updates the scrollbar location
---@param percent number @number between 0 and 1 representing the position of the middle part of the thumb
---@param percent number @number between 0 and 1 where 0 means thumb at the top and 1 at the bottom
function Scrollbar:set_percent(percent)
self.percent = percent
end

View File

@ -5,7 +5,7 @@ MOD_VERSION_MINOR = 0
MOD_VERSION_PATCH = 0
MOD_VERSION_STRING = string.format("%d.%d.%d", MOD_VERSION_MAJOR, MOD_VERSION_MINOR, MOD_VERSION_PATCH)
SCALE = tonumber(os.getenv("LITE_SCALE") or os.getenv("GDK_SCALE") or os.getenv("QT_SCALE_FACTOR")) or SCALE
SCALE = tonumber(os.getenv("LITE_SCALE") or os.getenv("GDK_SCALE") or os.getenv("QT_SCALE_FACTOR")) or 1
PATHSEP = package.config:sub(1, 1)
EXEDIR = EXEFILE:match("^(.+)[/\\][^/\\]+$")

View File

@ -232,15 +232,27 @@ function StatusView:register_docview_items()
return {
style.text, line, ":",
col > config.line_limit and style.accent or style.text, col,
style.text,
self.separator,
string.format("%.f%%", line / #dv.doc.lines * 100)
style.text
}
end,
command = "doc:go-to-line",
tooltip = "line : column"
})
self:add_item({
predicate = predicate_docview,
name = "doc:position-percent",
alignment = StatusView.Item.LEFT,
get_item = function()
local dv = core.active_view
local line = dv.doc:get_selection()
return {
string.format("%.f%%", line / #dv.doc.lines * 100)
}
end,
tooltip = "caret position"
})
self:add_item({
predicate = predicate_docview,
name = "doc:selections",
@ -307,6 +319,19 @@ function StatusView:register_docview_items()
end,
command = "doc:toggle-line-ending"
})
self:add_item {
predicate = predicate_docview,
name = "doc:overwrite-mode",
alignment = StatusView.Item.RIGHT,
get_item = function()
return {
style.text, core.active_view.doc.overwrite and "OVR" or "INS"
}
end,
command = "doc:toggle-overwrite",
separator = StatusView.separator2
}
end

View File

@ -3,7 +3,7 @@ local common = require "core.common"
local syntax = {}
syntax.items = {}
local plain_text_syntax = { name = "Plain Text", patterns = {}, symbols = {} }
syntax.plain_text_syntax = { name = "Plain Text", patterns = {}, symbols = {} }
function syntax.add(t)
@ -46,7 +46,7 @@ end
function syntax.get(filename, header)
return (filename and find(filename, "files"))
or (header and find(header, "headers"))
or plain_text_syntax
or syntax.plain_text_syntax
end

View File

@ -210,9 +210,11 @@ function tokenizer.tokenize(incoming_syntax, text, state, resume)
-- 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

View File

@ -142,14 +142,14 @@ function View:on_mouse_pressed(button, x, y, clicks)
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()
self.scroll.to.y = result * (self:get_scrollable_size() - self.size.y)
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()
self.scroll.to.x = result * (self:get_h_scrollable_size() - self.size.x)
end
return true
end
@ -177,7 +177,7 @@ function View:on_mouse_moved(x, y, dx, dy)
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()
self.scroll.to.y = result * (self:get_scrollable_size() - self.size.y)
if not config.animate_drag_scroll then
self:clamp_scroll_position()
self.scroll.y = self.scroll.to.y
@ -191,7 +191,7 @@ function View:on_mouse_moved(x, y, dx, dy)
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()
self.scroll.to.x = result * (self:get_h_scrollable_size() - self.size.x)
if not config.animate_drag_scroll then
self:clamp_scroll_position()
self.scroll.x = self.scroll.to.x
@ -287,12 +287,16 @@ 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)
local v_percent = self.scroll.y/(v_scrollable - self.size.y)
-- Avoid setting nan percent
self.v_scrollbar:set_percent(v_percent == v_percent and v_percent or 0)
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)
local h_percent = self.scroll.x/(h_scrollable - self.size.x)
-- Avoid setting nan percent
self.h_scrollbar:set_percent(h_percent == h_percent and h_percent or 0)
self.h_scrollbar:update()
end

View File

@ -10,6 +10,10 @@ local RootView = require "core.rootview"
local DocView = require "core.docview"
local Doc = require "core.doc"
---Symbols cache of all open documents
---@type table<core.doc, table>
local cache = setmetatable({}, { __mode = "k" })
config.plugins.autocomplete = common.merge({
-- Amount of characters that need to be written for autocomplete
min_len = 3,
@ -19,8 +23,16 @@ config.plugins.autocomplete = common.merge({
max_suggestions = 100,
-- Maximum amount of symbols to cache per document
max_symbols = 4000,
-- Which symbols to show on the suggestions list: global, local, related, none
suggestions_scope = "global",
-- Font size of the description box
desc_font_size = 12,
-- Do not show the icons associated to the suggestions
hide_icons = false,
-- Position where icons will be displayed on the suggestions list
icon_position = "left",
-- Do not show the additional information related to a suggestion
hide_info = false,
-- The config specification used by gui generators
config_spec = {
name = "Autocomplete",
@ -60,6 +72,26 @@ config.plugins.autocomplete = common.merge({
min = 1000,
max = 10000
},
{
label = "Suggestions Scope",
description = "Which symbols to show on the suggestions list.",
path = "suggestions_scope",
type = "selection",
default = "global",
values = {
{"All Documents", "global"},
{"Current Document", "local"},
{"Related Documents", "related"},
{"Known Symbols", "none"}
},
on_apply = function(value)
if value == "global" then
for _, doc in ipairs(core.docs) do
if cache[doc] then cache[doc] = nil end
end
end
end
},
{
label = "Description Font Size",
description = "Font size of the description box.",
@ -67,6 +99,31 @@ config.plugins.autocomplete = common.merge({
type = "number",
default = 12,
min = 8
},
{
label = "Hide Icons",
description = "Do not show icons on the suggestions list.",
path = "hide_icons",
type = "toggle",
default = false
},
{
label = "Icons Position",
description = "Position to display icons on the suggestions list.",
path = "icon_position",
type = "selection",
default = "left",
values = {
{"Left", "left"},
{"Right", "Right"}
}
},
{
label = "Hide Items Info",
description = "Do not show the additional info related to each suggestion.",
path = "hide_info",
type = "toggle",
default = false
}
}
}, config.plugins.autocomplete)
@ -76,6 +133,7 @@ local autocomplete = {}
autocomplete.map = {}
autocomplete.map_manually = {}
autocomplete.on_close = nil
autocomplete.icons = {}
-- Flag that indicates if the autocomplete box was manually triggered
-- with the autocomplete.complete() function to prevent the suggestions
@ -95,6 +153,7 @@ function autocomplete.add(t, manually_triggered)
{
text = text,
info = info.info,
icon = info.icon, -- Name of icon to show
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
@ -119,28 +178,35 @@ end
--
-- Thread that scans open document symbols and cache them
--
local max_symbols = config.plugins.autocomplete.max_symbols
local global_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
local function load_syntax_symbols(doc)
if doc.syntax and not autocomplete.map["language_"..doc.syntax.name] then
local symbols = {
name = "language_"..doc.syntax.name,
files = doc.syntax.files,
items = {}
}
for name, type in pairs(doc.syntax.symbols) do
symbols.items[name] = type
end
autocomplete.add(symbols)
return symbols.items
end
return {}
end
local function get_symbols(doc)
local s = {}
get_syntax_symbols(s, doc)
local syntax_symbols = load_syntax_symbols(doc)
local max_symbols = config.plugins.autocomplete.max_symbols
if doc.disable_symbols then return s end
local i = 1
local symbols_count = 0
while i <= #doc.lines do
for sym in doc.lines[i]:gmatch(config.symbol_pattern) do
if not s[sym] then
if not s[sym] and not syntax_symbols[sym] then
symbols_count = symbols_count + 1
if symbols_count > max_symbols then
s = nil
@ -186,14 +252,18 @@ core.add_thread(function()
}
end
-- update symbol set with doc's symbol set
if config.plugins.autocomplete.suggestions_scope == "global" then
for sym in pairs(cache[doc].symbols) do
symbols[sym] = true
end
end
coroutine.yield()
end
-- update symbols list
autocomplete.add { name = "open-docs", items = symbols }
-- update global symbols list
if config.plugins.autocomplete.suggestions_scope == "global" then
global_symbols = symbols
end
-- wait for next scan
local valid = true
@ -240,12 +310,50 @@ local function update_suggestions()
map = autocomplete.map_manually
end
local assigned_sym = {}
-- get all relevant suggestions for given filename
local items = {}
for _, v in pairs(map) do
if common.match_pattern(filename, v.files) then
for _, item in pairs(v.items) do
table.insert(items, item)
assigned_sym[item.text] = true
end
end
end
-- Append the global, local or related text symbols if applicable
local scope = config.plugins.autocomplete.suggestions_scope
if not triggered_manually then
local text_symbols = nil
if scope == "global" then
text_symbols = global_symbols
elseif scope == "local" and cache[doc] and cache[doc].symbols then
text_symbols = cache[doc].symbols
elseif scope == "related" then
for _, d in ipairs(core.docs) do
if doc.syntax == d.syntax then
if cache[d].symbols then
for name in pairs(cache[d].symbols) do
if not assigned_sym[name] then
table.insert(items, setmetatable(
{text = name, info = "normal"}, mt
))
end
end
end
end
end
end
if text_symbols then
for name in pairs(text_symbols) do
if not assigned_sym[name] then
table.insert(items, setmetatable({text = name, info = "normal"}, mt))
end
end
end
end
@ -286,13 +394,23 @@ local function get_suggestions_rect(av)
y = y + av:get_line_height() + style.padding.y
local font = av:get_font()
local th = font:get_height()
local has_icons = false
local hide_info = config.plugins.autocomplete.hide_info
local hide_icons = config.plugins.autocomplete.hide_icons
local max_width = 0
for _, s in ipairs(suggestions) do
local w = font:get_width(s.text)
if s.info then
if s.info and not hide_info then
w = w + style.font:get_width(s.info) + style.padding.x
end
local icon = s.icon or s.info
if not hide_icons and icon and autocomplete.icons[icon] then
w = w + autocomplete.icons[icon].font:get_width(
autocomplete.icons[icon].char
) + (style.padding.x / 2)
has_icons = true
end
max_width = math.max(max_width, w)
end
@ -319,7 +437,8 @@ local function get_suggestions_rect(av)
x - style.padding.x,
y - style.padding.y,
max_width + style.padding.x * 2,
max_items * (th + style.padding.y) + style.padding.y
max_items * (th + style.padding.y) + style.padding.y,
has_icons
end
local function wrap_line(line, max_chars)
@ -439,7 +558,7 @@ local function draw_suggestions_box(av)
local ah = config.plugins.autocomplete.max_height
-- draw background rect
local rx, ry, rw, rh = get_suggestions_rect(av)
local rx, ry, rw, rh, has_icons = get_suggestions_rect(av)
renderer.draw_rect(rx, ry, rw, rh, style.background3)
-- draw text
@ -448,17 +567,52 @@ local function draw_suggestions_box(av)
local y = ry + style.padding.y / 2
local show_count = #suggestions <= ah and #suggestions or ah
local start_index = suggestions_idx > ah and (suggestions_idx-(ah-1)) or 1
local hide_info = config.plugins.autocomplete.hide_info
for i=start_index, start_index+show_count-1, 1 do
if not suggestions[i] then
break
end
local s = suggestions[i]
local icon_l_padding, icon_r_padding = 0, 0
if has_icons then
local icon = s.icon or s.info
if icon and autocomplete.icons[icon] then
local ifont = autocomplete.icons[icon].font
local itext = autocomplete.icons[icon].char
local icolor = autocomplete.icons[icon].color
if i == suggestions_idx then
icolor = style.accent
elseif type(icolor) == "string" then
icolor = style.syntax[icolor]
end
if config.plugins.autocomplete.icon_position == "left" then
common.draw_text(
ifont, icolor, itext, "left", rx + style.padding.x, y, rw, lh
)
icon_l_padding = ifont:get_width(itext) + (style.padding.x / 2)
else
common.draw_text(
ifont, icolor, itext, "right", rx, y, rw - style.padding.x, lh
)
icon_r_padding = ifont:get_width(itext) + (style.padding.x / 2)
end
end
end
local color = (i == suggestions_idx) and style.accent or style.text
common.draw_text(font, color, s.text, "left", rx + style.padding.x, y, rw, lh)
if s.info then
common.draw_text(
font, color, s.text, "left",
rx + icon_l_padding + style.padding.x, y, rw, lh
)
if s.info and not hide_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)
common.draw_text(
style.font, color, s.info, "right",
rx, y, rw - icon_r_padding - style.padding.x, lh
)
end
y = y + lh
if suggestions_idx == i then
@ -619,6 +773,31 @@ function autocomplete.can_complete()
return false
end
---Register a font icon that can be assigned to completion items.
---@param name string
---@param character string
---@param font? renderer.font
---@param color? string | renderer.color A style.syntax[] name or specific color
function autocomplete.add_icon(name, character, font, color)
local color_type = type(color)
assert(
not color or color_type == "table"
or (color_type == "string" and style.syntax[color]),
"invalid icon color given"
)
autocomplete.icons[name] = {
char = character,
font = font or style.code_font,
color = color or "keyword"
}
end
--
-- Register built-in syntax symbol types icon
--
for name, _ in pairs(style.syntax) do
autocomplete.add_icon(name, "M", style.icon_font, name)
end
--
-- Commands
@ -632,7 +811,6 @@ 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)

View File

@ -266,7 +266,7 @@ local function detect_indent_stat(doc)
local max_lines = auto_detect_max_lines
for i, text in get_non_empty_lines(doc.syntax, doc.lines) do
local spaces = text:match("^ +")
if spaces then table.insert(stat, spaces:len()) end
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

View File

@ -347,7 +347,7 @@ end
command.add(nil, {
["draw-whitespace:toggle"] = function()
config.plugins.drawwhitespace.enabled = not config.drawwhitespace.enabled
config.plugins.drawwhitespace.enabled = not config.plugins.drawwhitespace.enabled
end,
["draw-whitespace:disable"] = function()

View File

@ -14,9 +14,9 @@ syntax.add {
{ pattern = { "/%*", "%*/" }, type = "comment" },
{ pattern = { '"', '"', '\\' }, type = "string" },
{ pattern = { "'", "'", '\\' }, type = "string" },
{ pattern = "0x%x+", type = "number" },
{ pattern = "0x%x+[%x']*", type = "number" },
{ pattern = "%d+[%d%.'eE]*f?", type = "number" },
{ pattern = "%.?%d+f?", type = "number" },
{ pattern = "%.?%d+[%d']*f?", type = "number" },
{ pattern = "[%+%-=/%*%^%%<>!~|:&]", type = "operator" },
{ pattern = "##", type = "operator" },
{ pattern = "struct%s()[%a_][%w_]*", type = {"keyword", "keyword2"} },

View File

@ -1,6 +1,56 @@
-- mod-version:3
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/R0w8Qw/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$" },
@ -9,16 +59,16 @@ syntax.add {
patterns = {
{ pattern = "//.*", type = "comment" },
{ pattern = { "/%*", "%*/" }, type = "comment" },
{ pattern = { '/[^= ]', '/', '\\' },type = "string" },
{ regex = regex_pattern, syntax = inner_regex_syntax, type = {"string", "string"} },
{ pattern = { '"', '"', '\\' }, type = "string" },
{ pattern = { "'", "'", '\\' }, type = "string" },
{ pattern = { "`", "`", '\\' }, type = "string" },
{ pattern = "0x[%da-fA-F_]+n?", type = "number" },
{ pattern = "-?%d+[%d%.eE_n]*", type = "number" },
{ pattern = "-?%.?%d+", type = "number" },
{ pattern = "0x[%da-fA-F_]+n?()%s*()/?", type = {"number", "normal", "operator"} },
{ pattern = "-?%d+[%d%.eE_n]*()%s*()/?", type = {"number", "normal", "operator"} },
{ pattern = "-?%.?%d+()%s*()/?", type = {"number", "normal", "operator"} },
{ pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
{ pattern = "[%a_][%w_]*%f[(]", type = "function" },
{ pattern = "[%a_][%w_]*", type = "symbol" },
{ pattern = "[%a_][%w_]*()%s*()/?", type = {"symbol", "normal", "operator"} },
},
symbols = {
["async"] = "keyword",

View File

@ -3,25 +3,6 @@ local syntax = require "core.syntax"
local style = require "core.style"
local core = require "core"
local initial_color = style.syntax["keyword2"]
-- Add 3 type of font styles for use on markdown files
for _, attr in pairs({"bold", "italic", "bold_italic"}) do
local attributes = {}
if attr ~= "bold_italic" then
attributes[attr] = true
else
attributes["bold"] = true
attributes["italic"] = true
end
style.syntax_fonts["markdown_"..attr] = style.code_font:copy(
style.code_font:get_size(),
attributes
)
-- also add a color for it
style.syntax["markdown_"..attr] = style.syntax["keyword2"]
end
local in_squares_match = "^%[%]"
local in_parenthesis_match = "^%(%)"
@ -225,12 +206,63 @@ syntax.add {
-- Adjust the color on theme changes
core.add_thread(function()
while true do
if initial_color ~= style.syntax["keyword2"] then
for _, attr in pairs({"bold", "italic", "bold_italic"}) do
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

View File

@ -219,7 +219,7 @@ function LineWrapping.draw_guide(docview)
end
function LineWrapping.update_docview_breaks(docview)
local x,y,w,h = docview.v_scrollbar:get_thumb_rect()
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

View File

@ -29,7 +29,7 @@ local tooltip_alpha_rate = 1
local function get_depth(filename)
local n = 1
for sep in filename:gmatch("[\\/]") do
for _ in filename:gmatch(PATHSEP) do
n = n + 1
end
return n

View File

@ -83,7 +83,8 @@ 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 },
text = not view.doc.filename and view.doc:get_text(1, 1, math.huge, math.huge)
crlf = view.doc.crlf,
text = view.doc.new_file and view.doc:get_text(1, 1, math.huge, math.huge)
}
end
if mt == LogView then return end
@ -106,7 +107,6 @@ 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,9 +114,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.scroll.x, dv.scroll.to.x = t.scroll.x, t.scroll.x

View File

@ -45,10 +45,14 @@ function dirmonitor:unwatch(fd_or_path) end
---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) end
function dirmonitor:check(callback, error_callback) end
---
---Get the working mode for the current file system monitoring backend.

View File

@ -61,10 +61,10 @@ 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
---@param timeout? number Amount of seconds, also supports fractions
---of a second, eg: 0.01. If not provided, waits forever.
---
---@return boolean status True on success or false if there was an error.
---@return boolean status True on success or false if there was an error or if no event was received.
function system.wait_event(timeout) end
---

View File

@ -4,8 +4,7 @@ project('lite-xl',
license : 'MIT',
meson_version : '>= 0.56',
default_options : [
'c_std=gnu11',
'wrap_mode=nofallback'
'c_std=gnu11'
]
)
@ -84,11 +83,10 @@ if not get_option('source-only')
'lua', # Fedora
]
if get_option('use_system_lua')
foreach lua : lua_names
last_lua = (lua == lua_names[-1] or get_option('wrap_mode') == 'forcefallback')
lua_dep = dependency(lua, fallback: last_lua ? ['lua', 'lua_dep'] : [], required : false,
version: '>= 5.4',
default_options: default_fallback_options + ['default_library=static', 'line_editing=false', 'interpreter=false']
lua_dep = dependency(lua, required : false,
)
if lua_dep.found()
break
@ -101,6 +99,11 @@ if not get_option('source-only')
lua_dep = cc.find_library('lua', required : true)
endif
endforeach
else
lua_dep = dependency('', fallback: ['lua', 'lua_dep'], required : true,
default_options: default_fallback_options + ['default_library=static', 'line_editing=disabled', 'interpreter=false']
)
endif
pcre2_dep = dependency('libpcre2-8', fallback: ['pcre2', 'libpcre2_8'],
default_options: default_fallback_options + ['default_library=static', 'grep=false', 'test=false']
@ -120,6 +123,7 @@ if not get_option('source-only')
sdl_options += 'use_atomic=enabled'
sdl_options += 'use_threads=enabled'
sdl_options += 'use_timers=enabled'
sdl_options += 'with_main=true'
# investigate if this is truly needed
# Do not remove before https://github.com/libsdl-org/SDL/issues/5413 is released
sdl_options += 'use_events=enabled'
@ -152,12 +156,24 @@ if not get_option('source-only')
sdl_options += 'use_video_vulkan=disabled'
sdl_options += 'use_video_offscreen=disabled'
sdl_options += 'use_power=disabled'
sdl_options += 'system_iconv=disabled'
sdl_dep = dependency('sdl2', fallback: ['sdl2', 'sdl2_dep'],
default_options: default_fallback_options + sdl_options
)
lite_deps = [lua_dep, sdl_dep, freetype_dep, pcre2_dep, libm, libdl]
if host_machine.system() == 'windows'
if sdl_dep.type_name() == 'internal'
sdlmain_dep = dependency('sdl2main', fallback: ['sdl2main_dep'])
else
sdlmain_dep = cc.find_library('SDL2main')
endif
else
sdlmain_dep = dependency('', required: false)
assert(not sdlmain_dep.found(), 'checking if fake dependency has been found')
endif
lite_deps = [lua_dep, sdl_dep, sdlmain_dep, freetype_dep, pcre2_dep, libm, libdl]
endif
#===============================================================================
# Install Configuration

View File

@ -4,3 +4,4 @@ option('portable', type : 'boolean', value : false, description: 'Portable insta
option('renderer', type : 'boolean', value : false, description: 'Use SDL renderer')
option('dirmonitor_backend', type : 'combo', value : '', choices : ['', 'inotify', 'fsevents', 'kqueue', 'win32', 'dummy'], description: 'define what dirmonitor backend to use')
option('arch_tuple', type : 'string', value : '', description: 'Specify a custom architecture tuple')
option('use_system_lua', type : 'boolean', value : false, description: 'Prefer System Lua over a the meson wrap')

View File

@ -11,8 +11,9 @@ This folder contains resources that is used for building or packaging the projec
- `icons/icon.{icns,ico,inl,rc,svg}`: lite-xl icon in various formats.
- `linux/com.lite_xl.LiteXL.appdata.xml`: AppStream metadata.
- `linux/com.lite_xl.LiteXL.desktop`: Desktop file for Linux desktops.
- `macos/appdmg.png`: Background image for packaging MacOS DMGs.
- `macos/Info.plist.in`: Template for generating `info.plist` on MacOS. See `macos/macos-retina-display.md` for details.
- `macos/dmg-cover.png`: Background image for packaging macOS DMGs.
- `macos/Info.plist.in`: Template for generating `info.plist` on macOS. See `macos/macos-retina-display.md` for details.
- `macos/lite-xl-dmg.py`: Configuration options for dmgbuild for packaging macOS DMGs.
- `windows/001-lua-unicode.diff`: Patch for allowing Lua to load files with UTF-8 filenames on Windows.
### Development

View File

@ -31,9 +31,14 @@
* An example command would be: gcc -shared -o xxxxx.so xxxxx.c
* You must not link to ANY lua library to avoid symbol collision.
*
* This file contains stock configuration for a typical installation of Lua 5.4.
* This file contains stock configuration for a typical installation of Lua 5.4.6.
* DO NOT MODIFY ANYTHING. MODIFYING STUFFS IN HERE WILL BREAK
* COMPATIBILITY WITH LITE XL AND CAUSE UNDEBUGGABLE BUGS.
*
* For reference, here are a list of permalinks to previous version of this file that targets an older version of Lua.
* If you don't need functionalities offered by the new version, use the OLDEST FILE for backwards compatibility.
*
* - Lua 5.4.4: https://github.com/lite-xl/lite-xl/blob/397973067f14420b26e3b20a238a50016c0b75e2/resources/include/lite_xl_plugin_api.h
**/
#ifndef LITE_XL_PLUGIN_API
#define LITE_XL_PLUGIN_API
@ -1028,6 +1033,7 @@ extern const char lua_ident[];
SYMBOL_DECLARE(lua_State *, lua_newstate, lua_Alloc f, void *ud)
SYMBOL_DECLARE(void, lua_close, lua_State *L)
SYMBOL_DECLARE(lua_State *, lua_newthread, lua_State *L)
SYMBOL_DECLARE(int, lua_closethread, lua_State *L, lua_State *from)
SYMBOL_DECLARE(int, lua_resetthread, lua_State *L)
SYMBOL_DECLARE(lua_CFunction, lua_atpanic, lua_State *L, lua_CFunction panicf)
@ -1739,6 +1745,9 @@ SYMBOL_WRAP_DECL(void, lua_close, lua_State *L) {
SYMBOL_WRAP_DECL(lua_State *, lua_newthread, lua_State *L) {
return SYMBOL_WRAP_CALL(lua_newthread, L);
}
SYMBOL_WRAP_DECL(int, lua_closethread, lua_State *L, lua_State *from) {
return SYMBOL_WRAP_CALL(lua_closethread, L, from);
}
SYMBOL_WRAP_DECL(int, lua_resetthread, lua_State *L) {
return SYMBOL_WRAP_CALL(lua_resetthread, L);
}
@ -2351,6 +2360,7 @@ void lite_xl_plugin_init(void *XL) {
IMPORT_SYMBOL(lua_newstate, lua_State *, lua_Alloc f, void *ud);
IMPORT_SYMBOL(lua_close, void, lua_State *L);
IMPORT_SYMBOL(lua_newthread, lua_State *, lua_State *L);
IMPORT_SYMBOL(lua_closethread, int, lua_State *L, lua_State *from);
IMPORT_SYMBOL(lua_resetthread, int, lua_State *L);
IMPORT_SYMBOL(lua_atpanic, lua_CFunction, lua_State *L, lua_CFunction panicf);
IMPORT_SYMBOL(lua_version, lua_Number, lua_State *L);

View File

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -0,0 +1,28 @@
# configuration for dmgbuild
import os.path
app_path = "Lite XL.app"
app_name = os.path.basename(app_path)
# Image options
format = defines.get("format", "UDZO")
# Content options
files = [(app_path, app_name)]
symlinks = { "Applications": "/Applications" }
icon = "resources/icons/icon.icns"
icon_locations = {
app_name: (144, 248),
"Applications": (336, 248)
}
# Window options
background = "resources/macos/dmg-cover.png"
window_rect = ((360, 360), (480, 380))
default_view = "coverflow"
include_icon_view_settings = True
# Icon view options
icon_size = 80
text_size = 11.0

View File

@ -1,6 +1,6 @@
diff -ruN lua-5.4.4\meson.build lua-5.4.4-patched\meson.build
--- lua-5.4.4\meson.build Wed Feb 22 18:16:56 2023
+++ lua-5.4.4-patched\meson.build Wed Feb 22 04:10:01 2023
diff -ruN lua-5.4.4/meson.build lua-5.4.4-patched/meson.build
--- lua-5.4.4/meson.build Wed Feb 22 18:16:56 2023
+++ lua-5.4.4-patched/meson.build Wed Feb 22 04:10:01 2023
@@ -85,6 +85,7 @@
'src/lutf8lib.c',
'src/lvm.c',
@ -9,9 +9,9 @@ diff -ruN lua-5.4.4\meson.build lua-5.4.4-patched\meson.build
dependencies: lua_lib_deps,
version: meson.project_version(),
soversion: lua_versions[0] + '.' + lua_versions[1],
diff -ruN lua-5.4.4\src\luaconf.h lua-5.4.4-patched\src\luaconf.h
--- lua-5.4.4\src\luaconf.h Thu Jan 13 19:24:43 2022
+++ lua-5.4.4-patched\src\luaconf.h Wed Feb 22 04:10:02 2023
diff -ruN lua-5.4.4/src/luaconf.h lua-5.4.4-patched/src/luaconf.h
--- lua-5.4.4/src/luaconf.h Thu Jan 13 19:24:43 2022
+++ lua-5.4.4-patched/src/luaconf.h Wed Feb 22 04:10:02 2023
@@ -782,5 +782,15 @@
@ -28,9 +28,9 @@ diff -ruN lua-5.4.4\src\luaconf.h lua-5.4.4-patched\src\luaconf.h
+
#endif
diff -ruN lua-5.4.4\src\Makefile lua-5.4.4-patched\src\Makefile
--- lua-5.4.4\src\Makefile Thu Jul 15 22:01:52 2021
+++ lua-5.4.4-patched\src\Makefile Wed Feb 22 04:10:02 2023
diff -ruN lua-5.4.4/src/Makefile lua-5.4.4-patched/src/Makefile
--- lua-5.4.4/src/Makefile Thu Jul 15 22:01:52 2021
+++ lua-5.4.4-patched/src/Makefile Wed Feb 22 04:10:02 2023
@@ -33,7 +33,7 @@
PLATS= guess aix bsd c89 freebsd generic linux linux-readline macosx mingw posix solaris
@ -40,9 +40,9 @@ diff -ruN lua-5.4.4\src\Makefile lua-5.4.4-patched\src\Makefile
LIB_O= lauxlib.o lbaselib.o lcorolib.o ldblib.o liolib.o lmathlib.o loadlib.o loslib.o lstrlib.o ltablib.o lutf8lib.o linit.o
BASE_O= $(CORE_O) $(LIB_O) $(MYOBJS)
diff -ruN lua-5.4.4\src\utf8_wrappers.c lua-5.4.4-patched\src\utf8_wrappers.c
--- lua-5.4.4\src\utf8_wrappers.c Thu Jan 01 08:00:00 1970
+++ lua-5.4.4-patched\src\utf8_wrappers.c Wed Feb 22 18:13:45 2023
diff -ruN lua-5.4.4/src/utf8_wrappers.c lua-5.4.4-patched/src/utf8_wrappers.c
--- lua-5.4.4/src/utf8_wrappers.c Thu Jan 01 08:00:00 1970
+++ lua-5.4.4-patched/src/utf8_wrappers.c Wed Feb 22 18:13:45 2023
@@ -0,0 +1,129 @@
+/**
+ * Wrappers to provide Unicode (UTF-8) support on Windows.
@ -173,9 +173,9 @@ diff -ruN lua-5.4.4\src\utf8_wrappers.c lua-5.4.4-patched\src\utf8_wrappers.c
+ return env_value;
+}
+#endif
diff -ruN lua-5.4.4\src\utf8_wrappers.h lua-5.4.4-patched\src\utf8_wrappers.h
--- lua-5.4.4\src\utf8_wrappers.h Thu Jan 01 08:00:00 1970
+++ lua-5.4.4-patched\src\utf8_wrappers.h Wed Feb 22 18:09:48 2023
diff -ruN lua-5.4.4/src/utf8_wrappers.h lua-5.4.4-patched/src/utf8_wrappers.h
--- lua-5.4.4/src/utf8_wrappers.h Thu Jan 01 08:00:00 1970
+++ lua-5.4.4-patched/src/utf8_wrappers.h Wed Feb 22 18:09:48 2023
@@ -0,0 +1,46 @@
+/**
+ * Wrappers to provide Unicode (UTF-8) support on Windows.

View File

@ -10,7 +10,7 @@ Various scripts and configurations used to configure, build, and package Lite XL
### Package
- **appdmg.sh**: Create a macOS DMG image using [AppDMG][1].
- **appdmg.sh**: Create a macOS DMG image using [dmgbuild][1].
- **appimage.sh**: [AppImage][2] builder.
- **innosetup.sh**: Creates a 32/64 bit [InnoSetup][3] installer package.
- **package.sh**: Creates all binary / DMG image / installer / source packages.
@ -25,6 +25,6 @@ Various scripts and configurations used to configure, build, and package Lite XL
- **generate_header.sh**: Generates a header file for native plugin API
- **keymap-generator**: Generates a JSON file containing the keymap
[1]: https://github.com/LinusU/node-appdmg
[1]: https://github.com/dmgbuild/dmgbuild
[2]: https://docs.appimage.org/
[3]: https://jrsoftware.org/isinfo.php

View File

@ -6,25 +6,4 @@ if [ ! -e "src/api/api.h" ]; then
exit 1
fi
cat > lite-xl-dmg.json << EOF
{
"title": "Lite XL",
"icon": "$(pwd)/resources/icons/icon.icns",
"background": "$(pwd)/resources/macos/appdmg.png",
"window": {
"position": {
"x": 360,
"y": 360
},
"size": {
"width": 480,
"height": 360
}
},
"contents": [
{ "x": 144, "y": 248, "type": "file", "path": "$(pwd)/Lite XL.app" },
{ "x": 336, "y": 248, "type": "link", "path": "/Applications" }
]
}
EOF
~/node_modules/appdmg/bin/appdmg.js lite-xl-dmg.json "$(pwd)/$1.dmg"
dmgbuild -s resources/macos/lite-xl-dmg.py "Lite XL" "$1.dmg"

View File

@ -181,7 +181,7 @@ main() {
# download the subprojects so we can start patching before configure.
# this will prevent reconfiguring the project.
meson subprojects download
lua_subproject_path=$(echo subprojects/lua-*/)
lua_subproject_path="subprojects/$(awk -F ' *= *' '/directory/ { printf $2 }' subprojects/lua.wrap)"
if [[ -d $lua_subproject_path ]]; then
patch -d $lua_subproject_path -p1 --forward < resources/windows/001-lua-unicode.diff
fi

View File

@ -1,7 +1,7 @@
#define MyAppName "Lite XL"
#define MyAppVersion "@PROJECT_VERSION@"
#define MyAppPublisher "Lite XL Team"
#define MyAppURL "https://lite-xl.github.io"
#define MyAppURL "https://lite-xl.com"
#define MyAppExeName "lite-xl.exe"
#define BuildDir "@PROJECT_BUILD_DIR@"
#define SourceDir "@PROJECT_SOURCE_DIR@"
@ -57,9 +57,13 @@ OutputBaseFilename=LiteXL-{#MyAppVersion}-{#ArchInternal}-setup
LicenseFile={#SourceDir}/LICENSE
SetupIconFile={#SourceDir}/resources/icons/icon.ico
UninstallDisplayIcon={app}\{#MyAppExeName}, 0
WizardImageFile="{#SourceDir}/scripts/innosetup/wizard-modern-image.bmp"
WizardSmallImageFile="{#SourceDir}/scripts/innosetup/litexl-55px.bmp"
; Required for the add to path option to refresh environment
ChangesEnvironment=yes
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
@ -67,11 +71,10 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 6.1; Check: not IsAdminInstallMode
Name: "portablemode"; Description: "Portable Mode"; Flags: unchecked
Name: "envPath"; Description: "Add lite-xl to the PATH variable, allowing it to be run from a command line."
[Files]
Source: "{#BuildDir}/src/lite-xl.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#BuildDir}/mingwLibs{#Arch}/*"; DestDir: "{app}"; Flags: ignoreversion ; Check: DirExists(ExpandConstant('{#BuildDir}/mingwLibs{#Arch}'))
Source: "{#SourceDir}/data/*"; DestDir: "{app}/data"; Flags: ignoreversion recursesubdirs
Source: "{#SourceDir}/lite-xl/*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
@ -81,8 +84,78 @@ Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}";
Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon; Check: not WizardIsTaskSelected('portablemode')
; Name: "{usersendto}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
[Registry]
Root: "HKA"; Subkey: "Software\Classes\*\shell\{#MyAppName}"; ValueType: string; ValueName: ""; ValueData: "Open with {#MyAppName}"; Flags: uninsdeletekey
Root: "HKA"; Subkey: "Software\Classes\*\shell\{#MyAppName}"; ValueType: string; ValueName: "Icon"; ValueData: "{app}\{#MyAppExeName}, 0"; Flags: uninsdeletekey
Root: "HKA"; Subkey: "Software\Classes\*\shell\{#MyAppName}\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExename}"" ""%1"""; Flags: uninsdeletekey
Root: "HKA"; Subkey: "Software\Classes\directory\shell\{#MyAppName}"; ValueType: string; ValueName: ""; ValueData: "Open with {#MyAppName}"; Flags: uninsdeletekey
Root: "HKA"; Subkey: "Software\Classes\directory\shell\{#MyAppName}"; ValueType: string; ValueName: "Icon"; ValueData: "{app}\{#MyAppExeName}, 0"; Flags: uninsdeletekey
Root: "HKA"; Subkey: "Software\Classes\directory\shell\{#MyAppName}\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExename}"" ""%1"""; Flags: uninsdeletekey
Root: "HKA"; Subkey: "Software\Classes\directory\background\shell\{#MyAppName}"; ValueType: string; ValueName: ""; ValueData: "Open with {#MyAppName}"; Flags: uninsdeletekey
Root: "HKA"; Subkey: "Software\Classes\directory\background\shell\{#MyAppName}"; ValueType: string; ValueName: "Icon"; ValueData: "{app}\{#MyAppExeName}, 0"; Flags: uninsdeletekey
Root: "HKA"; Subkey: "Software\Classes\directory\background\shell\{#MyAppName}\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExename}"" ""%V"""; Flags: uninsdeletekey
[Run]
Filename: "{app}/{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
[Setup]
Uninstallable=not WizardIsTaskSelected('portablemode')
; Code to add installation path to environment taken from:
; https://stackoverflow.com/a/46609047
[Code]
const EnvironmentKey = 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment';
procedure EnvAddPath(Path: string);
var
Paths: string;
begin
{ Retrieve current path (use empty string if entry not exists) }
if not RegQueryStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths)
then Paths := '';
{ Skip if string already found in path }
if Pos(';' + Uppercase(Path) + ';', ';' + Uppercase(Paths) + ';') > 0 then exit;
{ App string to the end of the path variable }
Paths := Paths + ';'+ Path +';'
{ Overwrite (or create if missing) path environment variable }
if RegWriteStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths)
then Log(Format('The [%s] added to PATH: [%s]', [Path, Paths]))
else Log(Format('Error while adding the [%s] to PATH: [%s]', [Path, Paths]));
end;
procedure EnvRemovePath(Path: string);
var
Paths: string;
P: Integer;
begin
{ Skip if registry entry not exists }
if not RegQueryStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths) then
exit;
{ Skip if string not found in path }
P := Pos(';' + Uppercase(Path) + ';', ';' + Uppercase(Paths) + ';');
if P = 0 then exit;
{ Update path variable }
Delete(Paths, P - 1, Length(Path) + 1);
{ Overwrite path environment variable }
if RegWriteStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths)
then Log(Format('The [%s] removed from PATH: [%s]', [Path, Paths]))
else Log(Format('Error while removing the [%s] from PATH: [%s]', [Path, Paths]));
end;
procedure CurStepChanged(CurStep: TSetupStep);
begin
if (CurStep = ssPostInstall) and WizardIsTaskSelected('envPath')
then EnvAddPath(ExpandConstant('{app}'));
end;
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
begin
if CurUninstallStep = usPostUninstall
then EnvRemovePath(ExpandConstant('{app}'));
end;

View File

@ -57,9 +57,7 @@ main() {
else
brew install bash ninja sdl2
fi
pip3 install meson
cd ~; npm install appdmg; cd -
~/node_modules/appdmg/bin/appdmg.js --version
pip3 install meson dmgbuild
elif [[ "$OSTYPE" == "msys" ]]; then
if [[ $lhelper == true ]]; then
pacman --noconfirm -S \

View File

@ -25,7 +25,7 @@ show_help() {
echo "-A --appimage Create an AppImage (Linux only)."
echo "-B --binary Create a normal / portable package or macOS bundle,"
echo " depending on how the build was configured. (Default.)"
echo "-D --dmg Create a DMG disk image with AppDMG (macOS only)."
echo "-D --dmg Create a DMG disk image with dmgbuild (macOS only)."
echo "-I --innosetup Create a InnoSetup package (Windows only)."
echo "-r --release Strip debugging symbols."
echo "-S --source Create a source code package,"
@ -264,6 +264,11 @@ main() {
$stripcmd "${exe_file}"
fi
if [[ $bundle == true ]]; then
# https://eclecticlight.co/2019/01/17/code-signing-for-the-concerned-3-signing-an-app/
codesign --force --deep -s - "${dest_dir}"
fi
echo "Creating a compressed archive ${package_name}"
if [[ $binary == true ]]; then
rm -f "${package_name}".tar.gz

View File

@ -1,4 +1,5 @@
#include "api.h"
#include "lua.h"
#include <SDL.h>
#include <stdlib.h>
#include <string.h>
@ -25,13 +26,16 @@ int get_mode_dirmonitor();
static int f_check_dir_callback(int watch_id, const char* path, void* L) {
lua_pushvalue(L, -1);
// using absolute indices from f_dirmonitor_check (2: callback, 3: error_callback)
lua_pushvalue(L, 2);
if (path)
lua_pushlstring(L, path, watch_id);
else
lua_pushnumber(L, watch_id);
lua_call(L, 1, 1);
int result = lua_toboolean(L, -1);
int result = 0;
if (lua_pcall(L, 1, 1, 3) == LUA_OK)
result = lua_toboolean(L, -1);
lua_pop(L, 1);
return !result;
}
@ -95,8 +99,20 @@ static int f_dirmonitor_unwatch(lua_State *L) {
}
static int f_noop(lua_State *L) { return 0; }
static int f_dirmonitor_check(lua_State* L) {
struct dirmonitor* monitor = luaL_checkudata(L, 1, API_TYPE_DIRMONITOR);
luaL_checktype(L, 2, LUA_TFUNCTION);
if (!lua_isnoneornil(L, 3)) {
luaL_checktype(L, 3, LUA_TFUNCTION);
} else {
lua_settop(L, 2);
lua_pushcfunction(L, f_noop);
}
lua_settop(L, 3);
SDL_LockMutex(monitor->mutex);
if (monitor->length < 0)
lua_pushnil(L);

View File

@ -31,6 +31,8 @@
typedef DWORD process_error_t;
typedef HANDLE process_stream_t;
typedef HANDLE process_handle_t;
typedef wchar_t process_arglist_t[32767];
typedef wchar_t *process_env_t;
#define HANDLE_INVALID (INVALID_HANDLE_VALUE)
#define PROCESS_GET_HANDLE(P) ((P)->process_information.hProcess)
@ -42,12 +44,20 @@ static volatile long PipeSerialNumber;
typedef int process_error_t;
typedef int process_stream_t;
typedef pid_t process_handle_t;
typedef char **process_arglist_t;
typedef char **process_env_t;
#define HANDLE_INVALID (0)
#define PROCESS_GET_HANDLE(P) ((P)->pid)
#endif
#ifdef __GNUC__
#define UNUSED __attribute__((__unused__))
#else
#define UNUSED
#endif
typedef struct {
bool running, detached;
int returncode, deadline;
@ -339,14 +349,248 @@ static bool signal_process(process_t* proc, signal_e sig) {
return true;
}
static UNUSED char *xstrdup(const char *str) {
char *result = str ? malloc(strlen(str) + 1) : NULL;
if (result) strcpy(result, str);
return result;
}
static int process_arglist_init(process_arglist_t *list, size_t *list_len, size_t nargs) {
*list_len = 0;
#ifdef _WIN32
memset(*list, 0, sizeof(process_arglist_t));
#else
*list = calloc(sizeof(char *), nargs + 1);
if (!*list) return ENOMEM;
#endif
return 0;
}
static int process_arglist_add(process_arglist_t *list, size_t *list_len, const char *arg, bool escape) {
size_t len = *list_len;
#ifdef _WIN32
int arg_len;
wchar_t *cmdline = *list;
wchar_t arg_w[32767];
// this length includes the null terminator!
if (!(arg_len = MultiByteToWideChar(CP_UTF8, 0, arg, -1, arg_w, 32767)))
return GetLastError();
if (arg_len + len > 32767)
return ERROR_NOT_ENOUGH_MEMORY;
if (!escape) {
// replace the current null terminator with a space
if (len > 0) cmdline[len-1] = ' ';
memcpy(cmdline + len, arg_w, arg_len * sizeof(wchar_t));
len += arg_len;
} else {
// if the string contains spaces, then we must quote it
bool quote = wcspbrk(arg_w, L" \t\v\r\n");
int backslash = 0, escaped_len = quote ? 2 : 0;
for (int i = 0; i < arg_len; i++) {
if (arg_w[i] == L'\\') {
backslash++;
} else if (arg_w[i] == L'"') {
escaped_len += backslash + 1;
backslash = 0;
} else {
backslash = 0;
}
escaped_len++;
}
// escape_len contains NUL terminator
if (escaped_len + len > 32767)
return ERROR_NOT_ENOUGH_MEMORY;
// replace our previous NUL terminator with space
if (len > 0) cmdline[len-1] = L' ';
if (quote) cmdline[len++] = L'"';
// we are not going to iterate over NUL terminator
for (int i = 0;arg_w[i]; i++) {
if (arg_w[i] == L'\\') {
backslash++;
} else if (arg_w[i] == L'"') {
// add backslash + 1 backslashes
for (int j = 0; j < backslash; j++)
cmdline[len++] = L'\\';
cmdline[len++] = L'\\';
backslash = 0;
} else {
backslash = 0;
}
cmdline[len++] = arg_w[i];
}
if (quote) cmdline[len++] = L'"';
cmdline[len++] = L'\0';
}
#else
char **cmd = *list;
cmd[len] = xstrdup(arg);
if (!cmd[len]) return ENOMEM;
len++;
#endif
*list_len = len;
return 0;
}
static void process_arglist_free(process_arglist_t *list) {
#ifndef _WIN32
char **cmd = *list;
for (int i = 0; cmd[i]; i++)
free(cmd[i]);
free(cmd);
*list = NULL;
#endif
}
static int process_env_init(process_env_t *env_list, size_t *env_len, size_t nenv) {
*env_len = 0;
#ifdef _WIN32
*env_list = NULL;
#else
*env_list = calloc(sizeof(char *), nenv * 2);
if (!*env_list) return ENOMEM;
#endif
return 0;
}
#ifdef _WIN32
static int cmp_name(wchar_t *a, wchar_t *b) {
wchar_t _A[32767], _B[32767], *A = _A, *B = _B, *a_eq, *b_eq;
int na, nb, r;
a_eq = wcschr(a, L'=');
b_eq = wcschr(b, L'=');
assert(a_eq);
assert(b_eq);
na = a_eq - a;
nb = b_eq - b;
r = LCMapStringW(LOCALE_INVARIANT, LCMAP_UPPERCASE, a, na, A, na);
assert(r == na);
A[na] = L'\0';
r = LCMapStringW(LOCALE_INVARIANT, LCMAP_UPPERCASE, b, nb, B, nb);
assert(r == nb);
B[nb] = L'\0';
for (;;) {
wchar_t AA = *A++, BB = *B++;
if (AA > BB)
return 1;
else if (AA < BB)
return -1;
else if (!AA && !BB)
return 0;
}
}
static int process_env_add_variable(process_env_t *env_list, size_t *env_list_len, wchar_t *var, size_t var_len) {
wchar_t *list, *list_p;
size_t block_var_len, list_len;
list = list_p = *env_list;
list_len = *env_list_len;
if (list_len) {
// check if it is already in the block
while ((block_var_len = wcslen(list_p))) {
if (cmp_name(list_p, var) == 0)
return -1; // already installed
list_p += block_var_len + 1;
}
}
// allocate list + 1 characters for the block terminator
list = realloc(list, (list_len + var_len + 1) * sizeof(wchar_t));
if (!list) return ERROR_NOT_ENOUGH_MEMORY;
// copy the env variable to the block
memcpy(list + list_len, var, var_len * sizeof(wchar_t));
// terminate the block again
list[list_len + var_len] = L'\0';
*env_list = list;
*env_list_len = (list_len + var_len);
return 0;
}
static int process_env_add_system(process_env_t *env_list, size_t *env_list_len) {
int retval = 0;
wchar_t *proc_env_block, *proc_env_block_p;
int proc_env_len;
proc_env_block = proc_env_block_p = GetEnvironmentStringsW();
while ((proc_env_len = wcslen(proc_env_block_p))) {
// try to add it to the list
if ((retval = process_env_add_variable(env_list, env_list_len, proc_env_block_p, proc_env_len + 1)) > 0)
goto cleanup;
proc_env_block_p += proc_env_len + 1;
}
retval = 0;
cleanup:
if (proc_env_block) FreeEnvironmentStringsW(proc_env_block);
return retval;
}
#endif
static int process_env_add(process_env_t *env_list, size_t *env_len, const char *key, const char *value) {
#ifdef _WIN32
wchar_t env_var[32767];
int r, var_len = 0;
if (!(r = MultiByteToWideChar(CP_UTF8, 0, key, -1, env_var, 32767)))
return GetLastError();
var_len += r;
env_var[var_len-1] = L'=';
if (!(r = MultiByteToWideChar(CP_UTF8, 0, value, -1, env_var + var_len, 32767 - var_len)))
return GetLastError();
var_len += r;
return process_env_add_variable(env_list, env_len, env_var, var_len);
#else
(*env_list)[*env_len] = xstrdup(key);
if (!(*env_list)[*env_len])
return ENOMEM;
(*env_list)[*env_len + 1] = xstrdup(value);
if (!(*env_list)[*env_len + 1])
return ENOMEM;
*env_len += 2;
#endif
return 0;
}
static void process_env_free(process_env_t *list) {
if (!*list) return;
#ifdef _WIN32
free(*list);
#else
for (size_t i = 0; (*list)[i]; i++) free((*list)[i]);
free(*list);
#endif
*list = NULL;
}
static int process_start(lua_State* L) {
int retval = 1;
size_t env_len = 0, key_len, val_len;
const char *cmd[256] = { NULL }, *env_names[256] = { NULL }, *env_values[256] = { NULL }, *cwd = NULL;
bool detach = false, literal = false;
int r, retval = 1;
size_t env_len = 0, cmd_len = 0, arglist_len = 0, env_vars_len = 0;
process_arglist_t arglist;
process_env_t env_vars = NULL;
const char *cwd = NULL;
bool detach = false, escape = true;
int deadline = 10, new_fds[3] = { STDIN_FD, STDOUT_FD, STDERR_FD };
size_t arg_len = lua_gettop(L), cmd_len;
if (lua_type(L, 1) == LUA_TTABLE) {
if (lua_isstring(L, 1)) {
escape = false;
// create a table that contains the string as the value
lua_createtable(L, 1, 0);
lua_pushvalue(L, 1);
lua_rawseti(L, -2, 1);
lua_replace(L, 1);
}
luaL_checktype(L, 1, LUA_TTABLE);
#if LUA_VERSION_NUM > 501
lua_len(L, 1);
#else
@ -354,36 +598,15 @@ static int process_start(lua_State* L) {
#endif
cmd_len = luaL_checknumber(L, -1); lua_pop(L, 1);
if (!cmd_len)
// we have not allocated anything here yet, so we can skip cleanup code
// don't do this anywhere else!
return luaL_argerror(L, 1, "table cannot be empty");
// check if each arguments is a string
for (size_t i = 1; i <= cmd_len; ++i) {
lua_pushinteger(L, i);
lua_rawget(L, 1);
cmd[i-1] = luaL_checkstring(L, -1);
}
} else {
literal = true;
cmd[0] = luaL_checkstring(L, 1);
cmd_len = 1;
lua_rawgeti(L, 1, i);
luaL_checkstring(L, -1);
lua_pop(L, 1);
}
if (arg_len > 1) {
lua_getfield(L, 2, "env");
if (!lua_isnil(L, -1)) {
lua_pushnil(L);
while (lua_next(L, -2) != 0) {
const char* key = luaL_checklstring(L, -2, &key_len);
const char* val = luaL_checklstring(L, -1, &val_len);
env_names[env_len] = malloc(key_len+1);
strcpy((char*)env_names[env_len], key);
env_values[env_len] = malloc(val_len+1);
strcpy((char*)env_values[env_len], val);
lua_pop(L, 1);
++env_len;
}
} else
lua_pop(L, 1);
if (lua_istable(L, 2)) {
lua_getfield(L, 2, "detach"); detach = lua_toboolean(L, -1);
lua_getfield(L, 2, "timeout"); deadline = luaL_optnumber(L, -1, deadline);
lua_getfield(L, 2, "cwd"); cwd = luaL_optstring(L, -1, NULL);
@ -391,12 +614,55 @@ static int process_start(lua_State* L) {
lua_getfield(L, 2, "stdout"); new_fds[STDOUT_FD] = luaL_optnumber(L, -1, STDOUT_FD);
lua_getfield(L, 2, "stderr"); new_fds[STDERR_FD] = luaL_optnumber(L, -1, STDERR_FD);
for (int stream = STDIN_FD; stream <= STDERR_FD; ++stream) {
if (new_fds[stream] > STDERR_FD || new_fds[stream] < REDIRECT_PARENT) {
lua_pushfstring(L, "error: redirect to handles, FILE* and paths are not supported");
if (new_fds[stream] > STDERR_FD || new_fds[stream] < REDIRECT_PARENT)
return luaL_error(L, "error: redirect to handles, FILE* and paths are not supported");
}
lua_pop(L, 6); // pop all the values above
luaL_getsubtable(L, 2, "env");
// count environment variobles
lua_pushnil(L);
while (lua_next(L, -2) != 0) {
luaL_checkstring(L, -2);
luaL_checkstring(L, -1);
lua_pop(L, 1);
env_len++;
}
if (env_len) {
if ((r = process_env_init(&env_vars, &env_vars_len, env_len)) != 0) {
retval = -1;
push_error(L, "cannot allocate environment list", r);
goto cleanup;
}
lua_pushnil(L);
while (lua_next(L, -2) != 0) {
if ((r = process_env_add(&env_vars, &env_vars_len, lua_tostring(L, -2), lua_tostring(L, -1))) != 0) {
retval = -1;
push_error(L, "cannot copy environment variable", r);
goto cleanup;
}
lua_pop(L, 1);
env_len++;
}
}
}
// allocate and copy commands
if ((r = process_arglist_init(&arglist, &arglist_len, cmd_len)) != 0) {
retval = -1;
push_error(L, "cannot create argument list", r);
goto cleanup;
}
for (size_t i = 1; i <= cmd_len; i++) {
lua_rawgeti(L, 1, i);
if ((r = process_arglist_add(&arglist, &arglist_len, lua_tostring(L, -1), escape)) != 0) {
retval = -1;
push_error(L, "cannot add argument", r);
goto cleanup;
}
lua_pop(L, 1);
}
process_t* self = lua_newuserdata(L, sizeof(process_t));
@ -405,6 +671,13 @@ static int process_start(lua_State* L) {
self->deadline = deadline;
self->detached = detach;
#if _WIN32
if (env_vars) {
if ((r = process_env_add_system(&env_vars, &env_vars_len)) != 0) {
retval = -1;
push_error(L, "cannot add environment variable", r);
goto cleanup;
}
}
for (int i = 0; i < 3; ++i) {
switch (new_fds[i]) {
case REDIRECT_PARENT:
@ -455,7 +728,7 @@ static int process_start(lua_State* L) {
self->child_pipes[i][1] = self->child_pipes[new_fds[i]][1];
}
}
STARTUPINFO siStartInfo;
STARTUPINFOW siStartInfo;
memset(&self->process_information, 0, sizeof(self->process_information));
memset(&siStartInfo, 0, sizeof(siStartInfo));
siStartInfo.cb = sizeof(siStartInfo);
@ -463,48 +736,10 @@ static int process_start(lua_State* L) {
siStartInfo.hStdInput = self->child_pipes[STDIN_FD][0];
siStartInfo.hStdOutput = self->child_pipes[STDOUT_FD][1];
siStartInfo.hStdError = self->child_pipes[STDERR_FD][1];
char commandLine[32767] = { 0 }, environmentBlock[32767], wideEnvironmentBlock[32767*2];
int offset = 0;
if (!literal) {
for (size_t i = 0; i < cmd_len; ++i) {
size_t len = strlen(cmd[i]);
if (offset + len + 2 >= sizeof(commandLine)) break;
if (i > 0)
commandLine[offset++] = ' ';
commandLine[offset++] = '"';
int backslashCount = 0; // Yes, this is necessary.
for (size_t j = 0; j < len && offset + 2 + backslashCount < sizeof(commandLine); ++j) {
if (cmd[i][j] == '\\')
++backslashCount;
else if (cmd[i][j] == '"') {
for (size_t k = 0; k < backslashCount; ++k)
commandLine[offset++] = '\\';
commandLine[offset++] = '\\';
backslashCount = 0;
} else
backslashCount = 0;
commandLine[offset++] = cmd[i][j];
}
if (offset + 1 + backslashCount >= sizeof(commandLine)) break;
for (size_t k = 0; k < backslashCount; ++k)
commandLine[offset++] = '\\';
commandLine[offset++] = '"';
}
commandLine[offset] = 0;
} else {
strncpy(commandLine, cmd[0], sizeof(commandLine));
}
offset = 0;
for (size_t i = 0; i < env_len; ++i) {
if (offset + strlen(env_values[i]) + strlen(env_names[i]) + 1 >= sizeof(environmentBlock))
break;
offset += snprintf(&environmentBlock[offset], sizeof(environmentBlock) - offset, "%s=%s", env_names[i], env_values[i]);
environmentBlock[offset++] = 0;
}
environmentBlock[offset++] = 0;
if (env_len > 0)
MultiByteToWideChar(CP_UTF8, MB_PRECOMPOSED, environmentBlock, offset, (LPWSTR)wideEnvironmentBlock, sizeof(wideEnvironmentBlock));
if (!CreateProcess(NULL, commandLine, NULL, NULL, true, (detach ? DETACHED_PROCESS : CREATE_NO_WINDOW) | CREATE_UNICODE_ENVIRONMENT, env_len > 0 ? wideEnvironmentBlock : NULL, cwd, &siStartInfo, &self->process_information)) {
wchar_t cwd_w[MAX_PATH];
if (cwd) // TODO: error handling
MultiByteToWideChar(CP_UTF8, 0, cwd, -1, cwd_w, MAX_PATH);
if (!CreateProcessW(NULL, arglist, NULL, NULL, true, (detach ? DETACHED_PROCESS : CREATE_NO_WINDOW) | CREATE_UNICODE_ENVIRONMENT, env_vars, cwd ? cwd_w : NULL, &siStartInfo, &self->process_information)) {
push_error(L, NULL, GetLastError());
retval = -1;
goto cleanup;
@ -552,9 +787,9 @@ static int process_start(lua_State* L) {
close(self->child_pipes[stream][stream == STDIN_FD ? 1 : 0]);
}
size_t set;
for (set = 0; set < env_len && setenv(env_names[set], env_values[set], 1) == 0; ++set);
if (set == env_len && (!detach || setsid() != -1) && (!cwd || chdir(cwd) != -1))
execvp(cmd[0], (char** const)cmd);
for (set = 0; set < env_vars_len && setenv(env_vars[set], env_vars[set+1], 1) == 0; set += 2);
if (set == env_vars_len && (!detach || setsid() != -1) && (!cwd || chdir(cwd) != -1))
execvp(arglist[0], (char** const)arglist);
write(control_pipe[1], &errno, sizeof(errno));
_exit(-1);
}
@ -588,16 +823,15 @@ static int process_start(lua_State* L) {
if (control_pipe[0]) close(control_pipe[0]);
if (control_pipe[1]) close(control_pipe[1]);
#endif
for (size_t i = 0; i < env_len; ++i) {
free((char*)env_names[i]);
free((char*)env_values[i]);
}
for (int stream = 0; stream < 3; ++stream) {
process_stream_t* pipe = &self->child_pipes[stream][stream == STDIN_FD ? 0 : 1];
if (*pipe) {
close_fd(pipe);
}
}
process_arglist_free(&arglist);
process_env_free(&env_vars);
if (retval == -1)
return lua_error(L);

View File

@ -90,7 +90,7 @@ static int f_font_load(lua_State *L) {
return ret_code;
RenFont** font = lua_newuserdata(L, sizeof(RenFont*));
*font = ren_font_load(&window_renderer, filename, size, antialiasing, hinting, style);
*font = ren_font_load(window_renderer, filename, size, antialiasing, hinting, style);
if (!*font)
return luaL_error(L, "failed to load font");
luaL_setmetatable(L, API_TYPE_FONT);
@ -130,7 +130,7 @@ static int f_font_copy(lua_State *L) {
}
for (int i = 0; i < FONT_FALLBACK_MAX && fonts[i]; ++i) {
RenFont** font = lua_newuserdata(L, sizeof(RenFont*));
*font = ren_font_copy(&window_renderer, fonts[i], size, antialiasing, hinting, style);
*font = ren_font_copy(window_renderer, fonts[i], size, antialiasing, hinting, style);
if (!*font)
return luaL_error(L, "failed to copy font");
luaL_setmetatable(L, API_TYPE_FONT);
@ -198,7 +198,7 @@ static int f_font_get_width(lua_State *L) {
size_t len;
const char *text = luaL_checklstring(L, 2, &len);
lua_pushnumber(L, ren_font_group_get_width(&window_renderer, fonts, text, len));
lua_pushnumber(L, ren_font_group_get_width(window_renderer, fonts, text, len, NULL));
return 1;
}
@ -217,7 +217,7 @@ static int f_font_get_size(lua_State *L) {
static int f_font_set_size(lua_State *L) {
RenFont* fonts[FONT_FALLBACK_MAX]; font_retrieve(L, fonts, 1);
float size = luaL_checknumber(L, 2);
ren_font_group_set_size(&window_renderer, fonts, size);
ren_font_group_set_size(window_renderer, fonts, size);
return 0;
}
@ -276,7 +276,7 @@ static int f_show_debug(lua_State *L) {
static int f_get_size(lua_State *L) {
int w, h;
ren_get_size(&window_renderer, &w, &h);
ren_get_size(window_renderer, &w, &h);
lua_pushnumber(L, w);
lua_pushnumber(L, h);
return 2;
@ -284,13 +284,13 @@ static int f_get_size(lua_State *L) {
static int f_begin_frame(UNUSED lua_State *L) {
rencache_begin_frame(&window_renderer);
rencache_begin_frame(window_renderer);
return 0;
}
static int f_end_frame(UNUSED lua_State *L) {
rencache_end_frame(&window_renderer);
rencache_end_frame(window_renderer);
// clear the font reference table
lua_newtable(L);
lua_rawseti(L, LUA_REGISTRYINDEX, RENDERER_FONT_REF);
@ -311,7 +311,7 @@ static int f_set_clip_rect(lua_State *L) {
lua_Number w = luaL_checknumber(L, 3);
lua_Number h = luaL_checknumber(L, 4);
RenRect rect = rect_to_grid(x, y, w, h);
rencache_set_clip_rect(&window_renderer, rect);
rencache_set_clip_rect(window_renderer, rect);
return 0;
}
@ -323,7 +323,7 @@ static int f_draw_rect(lua_State *L) {
lua_Number h = luaL_checknumber(L, 4);
RenRect rect = rect_to_grid(x, y, w, h);
RenColor color = checkcolor(L, 5, 255);
rencache_draw_rect(&window_renderer, rect, color);
rencache_draw_rect(window_renderer, rect, color);
return 0;
}
@ -348,7 +348,7 @@ static int f_draw_text(lua_State *L) {
double x = luaL_checknumber(L, 3);
int y = luaL_checknumber(L, 4);
RenColor color = checkcolor(L, 5, 255);
x = rencache_draw_text(&window_renderer, fonts, text, len, x, y, color);
x = rencache_draw_text(window_renderer, fonts, text, len, x, y, color);
lua_pushnumber(L, x);
return 1;
}

View File

@ -74,7 +74,7 @@ static SDL_HitTestResult SDLCALL hit_test(SDL_Window *window, const SDL_Point *p
const int controls_width = hit_info->controls_width;
int w, h;
SDL_GetWindowSize(window_renderer.window, &w, &h);
SDL_GetWindowSize(window_renderer->window, &w, &h);
if (pt->y < hit_info->title_height &&
#if RESIZE_FROM_TOP
@ -186,7 +186,7 @@ top:
case SDL_WINDOWEVENT:
if (e.window.event == SDL_WINDOWEVENT_RESIZED) {
ren_resize_window(&window_renderer);
ren_resize_window(window_renderer);
lua_pushstring(L, "resized");
/* The size below will be in points. */
lua_pushinteger(L, e.window.data1);
@ -225,8 +225,8 @@ top:
SDL_GetMouseState(&mx, &my);
lua_pushstring(L, "filedropped");
lua_pushstring(L, e.drop.file);
lua_pushinteger(L, mx);
lua_pushinteger(L, my);
lua_pushinteger(L, mx * window_renderer->scale_x);
lua_pushinteger(L, my * window_renderer->scale_y);
SDL_free(e.drop.file);
return 4;
@ -283,8 +283,8 @@ top:
if (e.button.button == 1) { SDL_CaptureMouse(1); }
lua_pushstring(L, "mousepressed");
lua_pushstring(L, button_name(e.button.button));
lua_pushinteger(L, e.button.x);
lua_pushinteger(L, e.button.y);
lua_pushinteger(L, e.button.x * window_renderer->scale_x);
lua_pushinteger(L, e.button.y * window_renderer->scale_y);
lua_pushinteger(L, e.button.clicks);
return 5;
@ -292,8 +292,8 @@ top:
if (e.button.button == 1) { SDL_CaptureMouse(0); }
lua_pushstring(L, "mousereleased");
lua_pushstring(L, button_name(e.button.button));
lua_pushinteger(L, e.button.x);
lua_pushinteger(L, e.button.y);
lua_pushinteger(L, e.button.x * window_renderer->scale_x);
lua_pushinteger(L, e.button.y * window_renderer->scale_y);
return 4;
case SDL_MOUSEMOTION:
@ -305,10 +305,10 @@ top:
e.motion.yrel += event_plus.motion.yrel;
}
lua_pushstring(L, "mousemoved");
lua_pushinteger(L, e.motion.x);
lua_pushinteger(L, e.motion.y);
lua_pushinteger(L, e.motion.xrel);
lua_pushinteger(L, e.motion.yrel);
lua_pushinteger(L, e.motion.x * window_renderer->scale_x);
lua_pushinteger(L, e.motion.y * window_renderer->scale_y);
lua_pushinteger(L, e.motion.xrel * window_renderer->scale_x);
lua_pushinteger(L, e.motion.yrel * window_renderer->scale_y);
return 5;
case SDL_MOUSEWHEEL:
@ -324,7 +324,7 @@ top:
return 3;
case SDL_FINGERDOWN:
SDL_GetWindowSize(window_renderer.window, &w, &h);
SDL_GetWindowSize(window_renderer->window, &w, &h);
lua_pushstring(L, "touchpressed");
lua_pushinteger(L, (lua_Integer)(e.tfinger.x * w));
@ -333,7 +333,7 @@ top:
return 4;
case SDL_FINGERUP:
SDL_GetWindowSize(window_renderer.window, &w, &h);
SDL_GetWindowSize(window_renderer->window, &w, &h);
lua_pushstring(L, "touchreleased");
lua_pushinteger(L, (lua_Integer)(e.tfinger.x * w));
@ -349,7 +349,7 @@ top:
e.tfinger.dx += event_plus.tfinger.dx;
e.tfinger.dy += event_plus.tfinger.dy;
}
SDL_GetWindowSize(window_renderer.window, &w, &h);
SDL_GetWindowSize(window_renderer->window, &w, &h);
lua_pushstring(L, "touchmoved");
lua_pushinteger(L, (lua_Integer)(e.tfinger.x * w));
@ -363,7 +363,7 @@ top:
#ifdef LITE_USE_SDL_RENDERER
rencache_invalidate();
#else
SDL_UpdateWindowSurface(window_renderer.window);
SDL_UpdateWindowSurface(window_renderer->window);
#endif
lua_pushstring(L, e.type == SDL_APP_WILLENTERFOREGROUND ? "enteringforeground" : "enteredforeground");
return 1;
@ -386,6 +386,7 @@ static int f_wait_event(lua_State *L) {
int nargs = lua_gettop(L);
if (nargs >= 1) {
double n = luaL_checknumber(L, 1);
if (n < 0) n = 0;
lua_pushboolean(L, SDL_WaitEventTimeout(NULL, n * 1000));
} else {
lua_pushboolean(L, SDL_WaitEvent(NULL));
@ -428,7 +429,7 @@ static int f_set_cursor(lua_State *L) {
static int f_set_window_title(lua_State *L) {
const char *title = luaL_checkstring(L, 1);
SDL_SetWindowTitle(window_renderer.window, title);
SDL_SetWindowTitle(window_renderer->window, title);
return 0;
}
@ -438,39 +439,39 @@ enum { WIN_NORMAL, WIN_MINIMIZED, WIN_MAXIMIZED, WIN_FULLSCREEN };
static int f_set_window_mode(lua_State *L) {
int n = luaL_checkoption(L, 1, "normal", window_opts);
SDL_SetWindowFullscreen(window_renderer.window,
SDL_SetWindowFullscreen(window_renderer->window,
n == WIN_FULLSCREEN ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0);
if (n == WIN_NORMAL) { SDL_RestoreWindow(window_renderer.window); }
if (n == WIN_MAXIMIZED) { SDL_MaximizeWindow(window_renderer.window); }
if (n == WIN_MINIMIZED) { SDL_MinimizeWindow(window_renderer.window); }
if (n == WIN_NORMAL) { SDL_RestoreWindow(window_renderer->window); }
if (n == WIN_MAXIMIZED) { SDL_MaximizeWindow(window_renderer->window); }
if (n == WIN_MINIMIZED) { SDL_MinimizeWindow(window_renderer->window); }
return 0;
}
static int f_set_window_bordered(lua_State *L) {
int bordered = lua_toboolean(L, 1);
SDL_SetWindowBordered(window_renderer.window, bordered);
SDL_SetWindowBordered(window_renderer->window, bordered);
return 0;
}
static int f_set_window_hit_test(lua_State *L) {
if (lua_gettop(L) == 0) {
SDL_SetWindowHitTest(window_renderer.window, NULL, NULL);
SDL_SetWindowHitTest(window_renderer->window, NULL, NULL);
return 0;
}
window_hit_info->title_height = luaL_checknumber(L, 1);
window_hit_info->controls_width = luaL_checknumber(L, 2);
window_hit_info->resize_border = luaL_checknumber(L, 3);
SDL_SetWindowHitTest(window_renderer.window, hit_test, window_hit_info);
SDL_SetWindowHitTest(window_renderer->window, hit_test, window_hit_info);
return 0;
}
static int f_get_window_size(lua_State *L) {
int x, y, w, h;
SDL_GetWindowSize(window_renderer.window, &w, &h);
SDL_GetWindowPosition(window_renderer.window, &x, &y);
SDL_GetWindowSize(window_renderer->window, &w, &h);
SDL_GetWindowPosition(window_renderer->window, &x, &y);
lua_pushinteger(L, w);
lua_pushinteger(L, h);
lua_pushinteger(L, x);
@ -484,22 +485,22 @@ static int f_set_window_size(lua_State *L) {
double h = luaL_checknumber(L, 2);
double x = luaL_checknumber(L, 3);
double y = luaL_checknumber(L, 4);
SDL_SetWindowSize(window_renderer.window, w, h);
SDL_SetWindowPosition(window_renderer.window, x, y);
ren_resize_window(&window_renderer);
SDL_SetWindowSize(window_renderer->window, w, h);
SDL_SetWindowPosition(window_renderer->window, x, y);
ren_resize_window(window_renderer);
return 0;
}
static int f_window_has_focus(lua_State *L) {
unsigned flags = SDL_GetWindowFlags(window_renderer.window);
unsigned flags = SDL_GetWindowFlags(window_renderer->window);
lua_pushboolean(L, flags & SDL_WINDOW_INPUT_FOCUS);
return 1;
}
static int f_get_window_mode(lua_State *L) {
unsigned flags = SDL_GetWindowFlags(window_renderer.window);
unsigned flags = SDL_GetWindowFlags(window_renderer->window);
if (flags & SDL_WINDOW_FULLSCREEN_DESKTOP) {
lua_pushstring(L, "fullscreen");
} else if (flags & SDL_WINDOW_MINIMIZED) {
@ -537,8 +538,8 @@ static int f_raise_window(lua_State *L) {
to allow the window to be focused. Also on wayland the raise window event
may not always be obeyed.
*/
SDL_SetWindowInputFocus(window_renderer.window);
SDL_RaiseWindow(window_renderer.window);
SDL_SetWindowInputFocus(window_renderer->window);
SDL_RaiseWindow(window_renderer->window);
return 0;
}
@ -861,6 +862,7 @@ static int f_get_time(lua_State *L) {
static int f_sleep(lua_State *L) {
double n = luaL_checknumber(L, 1);
if (n < 0) n = 0;
SDL_Delay(n * 1000);
return 0;
}
@ -914,7 +916,7 @@ static int f_fuzzy_match(lua_State *L) {
static int f_set_window_opacity(lua_State *L) {
double n = luaL_checknumber(L, 1);
int r = SDL_SetWindowOpacity(window_renderer.window, n);
int r = SDL_SetWindowOpacity(window_renderer->window, n);
lua_pushboolean(L, r > -1);
return 1;
}
@ -1056,7 +1058,7 @@ static int f_load_native_plugin(lua_State *L) {
#endif
/* Special purpose filepath compare function. Corresponds to the
order used in the TreeView view of the project's files. Returns true iff
order used in the TreeView view of the project's files. Returns true if
path1 < path2 in the TreeView order. */
static int f_path_compare(lua_State *L) {
size_t len1, len2;
@ -1070,7 +1072,6 @@ static int f_path_compare(lua_State *L) {
size_t offset = 0, i, j;
for (i = 0; i < len1 && i < len2; i++) {
if (path1[i] != path2[i]) break;
if (isdigit(path1[i])) break;
if (path1[i] == PATHSEP) {
offset = i + 1;
}

View File

@ -20,16 +20,6 @@
static SDL_Window *window;
static double get_scale(void) {
#ifndef __APPLE__
float dpi;
if (SDL_GetDisplayDPI(0, NULL, &dpi, NULL) == 0)
return dpi / 96.0;
#endif
return 1.0;
}
static void get_exe_filename(char *buf, int sz) {
#if _WIN32
int len;
@ -170,6 +160,8 @@ int main(int argc, char **argv) {
SDL_SetHint("SDL_MOUSE_DOUBLE_CLICK_RADIUS", "4");
#endif
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
SDL_DisplayMode dm;
SDL_GetCurrentDisplayMode(0, &dm);
@ -181,7 +173,7 @@ int main(int argc, char **argv) {
fprintf(stderr, "Error creating lite-xl window: %s", SDL_GetError());
exit(1);
}
ren_init(window);
window_renderer = ren_init(window);
lua_State *L;
init_lua:
@ -203,9 +195,6 @@ init_lua:
lua_pushstring(L, LITE_ARCH_TUPLE);
lua_setglobal(L, "ARCH");
lua_pushnumber(L, get_scale());
lua_setglobal(L, "SCALE");
char exename[2048];
get_exe_filename(exename, sizeof(exename));
if (*exename) {
@ -275,7 +264,7 @@ init_lua:
// This allows the window to be destroyed before lite-xl is done with
// reaping child processes
ren_free_window_resources(&window_renderer);
ren_free(window_renderer);
lua_close(L);
return EXIT_SUCCESS;

View File

@ -191,8 +191,9 @@ void rencache_draw_rect(RenWindow *window_renderer, RenRect rect, RenColor color
double rencache_draw_text(RenWindow *window_renderer, RenFont **fonts, const char *text, size_t len, double x, int y, RenColor color)
{
double width = ren_font_group_get_width(window_renderer, fonts, text, len);
RenRect rect = { x, y, (int)width, ren_font_group_get_height(fonts) };
int x_offset;
double width = ren_font_group_get_width(window_renderer, fonts, text, len, &x_offset);
RenRect rect = { x + x_offset, y, (int)(width - x_offset), ren_font_group_get_height(fonts) };
if (rects_overlap(last_clip_rect, rect)) {
int sz = len + 1;
DrawTextCommand *cmd = push_command(window_renderer, DRAW_TEXT, sizeof(DrawTextCommand) + sz);

View File

@ -22,7 +22,7 @@
#define MAX_LOADABLE_GLYPHSETS (MAX_UNICODE / GLYPHSET_SIZE)
#define SUBPIXEL_BITMAPS_CACHED 3
RenWindow window_renderer = {0};
RenWindow* window_renderer = NULL;
static FT_Library library;
// draw_rect_surface is used as a 1x1 surface to simplify ren_draw_rect with blending
@ -167,7 +167,7 @@ static void font_load_glyphset(RenFont* font, int idx) {
for (unsigned int column = 0; column < slot->bitmap.width; ++column) {
int current_source_offset = source_offset + (column / 8);
int source_pixel = slot->bitmap.buffer[current_source_offset];
pixels[++target_offset] = ((source_pixel >> (7 - (column % 8))) & 0x1) << 7;
pixels[++target_offset] = ((source_pixel >> (7 - (column % 8))) & 0x1) * 0xFF;
}
} else
memcpy(&pixels[target_offset], &slot->bitmap.buffer[source_offset], slot->bitmap.width);
@ -348,10 +348,11 @@ int ren_font_group_get_height(RenFont **fonts) {
return fonts[0]->height;
}
double ren_font_group_get_width(RenWindow *window_renderer, RenFont **fonts, const char *text, size_t len) {
double ren_font_group_get_width(RenWindow *window_renderer, RenFont **fonts, const char *text, size_t len, int *x_offset) {
double width = 0;
const char* end = text + len;
GlyphMetric* metric = NULL; GlyphSet* set = NULL;
bool set_x_offset = x_offset == NULL;
while (text < end) {
unsigned int codepoint;
text = utf8_to_codepoint(text, &codepoint);
@ -359,8 +360,15 @@ double ren_font_group_get_width(RenWindow *window_renderer, RenFont **fonts, con
if (!metric)
break;
width += (!font || metric->xadvance) ? metric->xadvance : fonts[0]->space_advance;
if (!set_x_offset) {
set_x_offset = true;
*x_offset = metric->bitmap_left; // TODO: should this be scaled by the surface scale?
}
}
const int surface_scale = renwin_get_surface(window_renderer).scale;
if (!set_x_offset) {
*x_offset = 0;
}
return width / surface_scale;
}
@ -493,33 +501,38 @@ void ren_draw_rect(RenSurface *rs, RenRect rect, RenColor color) {
}
/*************** Window Management ****************/
void ren_free_window_resources(RenWindow *window_renderer) {
RenWindow* ren_init(SDL_Window *win) {
assert(win);
int error = FT_Init_FreeType( &library );
if ( error ) {
fprintf(stderr, "internal font error when starting the application\n");
return NULL;
}
RenWindow* window_renderer = malloc(sizeof(RenWindow));
window_renderer->window = win;
renwin_init_surface(window_renderer);
renwin_init_command_buf(window_renderer);
renwin_clip_to_surface(window_renderer);
draw_rect_surface = SDL_CreateRGBSurface(0, 1, 1, 32,
0xFF000000, 0x00FF0000, 0x0000FF00, 0x000000FF);
return window_renderer;
}
void ren_free(RenWindow* window_renderer) {
assert(window_renderer);
renwin_free(window_renderer);
SDL_FreeSurface(draw_rect_surface);
free(window_renderer->command_buf);
window_renderer->command_buf = NULL;
window_renderer->command_buf_size = 0;
free(window_renderer);
}
// TODO remove global and return RenWindow*
void ren_init(SDL_Window *win) {
assert(win);
int error = FT_Init_FreeType( &library );
if ( error ) {
fprintf(stderr, "internal font error when starting the application\n");
return;
}
window_renderer.window = win;
renwin_init_surface(&window_renderer);
renwin_init_command_buf(&window_renderer);
renwin_clip_to_surface(&window_renderer);
draw_rect_surface = SDL_CreateRGBSurface(0, 1, 1, 32,
0xFF000000, 0x00FF0000, 0x0000FF00, 0x000000FF);
}
void ren_resize_window(RenWindow *window_renderer) {
renwin_resize_surface(window_renderer);
renwin_update_scale(window_renderer);
}

View File

@ -23,7 +23,7 @@ typedef struct { SDL_Surface *surface; int scale; } RenSurface;
struct RenWindow;
typedef struct RenWindow RenWindow;
extern RenWindow window_renderer;
extern RenWindow* window_renderer;
RenFont* ren_font_load(RenWindow *window_renderer, const char *filename, float size, ERenFontAntialiasing antialiasing, ERenFontHinting hinting, unsigned char style);
RenFont* ren_font_copy(RenWindow *window_renderer, RenFont* font, float size, ERenFontAntialiasing antialiasing, ERenFontHinting hinting, int style);
@ -34,17 +34,17 @@ int ren_font_group_get_height(RenFont **font);
float ren_font_group_get_size(RenFont **font);
void ren_font_group_set_size(RenWindow *window_renderer, RenFont **font, float size);
void ren_font_group_set_tab_size(RenFont **font, int n);
double ren_font_group_get_width(RenWindow *window_renderer, RenFont **font, const char *text, size_t len);
double ren_font_group_get_width(RenWindow *window_renderer, RenFont **font, const char *text, size_t len, int *x_offset);
double ren_draw_text(RenSurface *rs, RenFont **font, const char *text, size_t len, float x, int y, RenColor color);
void ren_draw_rect(RenSurface *rs, RenRect rect, RenColor color);
void ren_init(SDL_Window *win);
RenWindow* ren_init(SDL_Window *win);
void ren_free(RenWindow* window_renderer);
void ren_resize_window(RenWindow *window_renderer);
void ren_update_rects(RenWindow *window_renderer, RenRect *rects, int count);
void ren_set_clip_rect(RenWindow *window_renderer, RenRect rect);
void ren_get_size(RenWindow *window_renderer, int *x, int *y); /* Reports the size in points. */
void ren_free_window_resources(RenWindow *window_renderer);
#endif

View File

@ -29,7 +29,8 @@ static void setup_renderer(RenWindow *ren, int w, int h) {
#endif
void renwin_init_surface(UNUSED RenWindow *ren) {
void renwin_init_surface(RenWindow *ren) {
ren->scale_x = ren->scale_y = 1;
#ifdef LITE_USE_SDL_RENDERER
if (ren->rensurface.surface) {
SDL_FreeSurface(ren->rensurface.surface);
@ -95,6 +96,16 @@ void renwin_resize_surface(UNUSED RenWindow *ren) {
#endif
}
void renwin_update_scale(RenWindow *ren) {
#ifndef LITE_USE_SDL_RENDERER
SDL_Surface *surface = SDL_GetWindowSurface(ren->window);
int window_w = surface->w, window_h = surface->h;
SDL_GetWindowSize(ren->window, &window_w, &window_h);
ren->scale_x = (float)surface->w / window_w;
ren->scale_y = (float)surface->h / window_h;
#endif
}
void renwin_show_window(RenWindow *ren) {
SDL_ShowWindow(ren->window);
}

View File

@ -6,6 +6,8 @@ struct RenWindow {
uint8_t *command_buf;
size_t command_buf_idx;
size_t command_buf_size;
float scale_x;
float scale_y;
#ifdef LITE_USE_SDL_RENDERER
SDL_Renderer *renderer;
SDL_Texture *texture;
@ -19,6 +21,7 @@ void renwin_init_command_buf(RenWindow *ren);
void renwin_clip_to_surface(RenWindow *ren);
void renwin_set_clip_rect(RenWindow *ren, RenRect rect);
void renwin_resize_surface(RenWindow *ren);
void renwin_update_scale(RenWindow *ren);
void renwin_show_window(RenWindow *ren);
void renwin_update_rects(RenWindow *ren, RenRect *rects, int count);
void renwin_free(RenWindow *ren);

View File

@ -1,9 +1,10 @@
[wrap-file]
directory = freetype-2.12.1
source_url = https://download.savannah.gnu.org/releases/freetype/freetype-2.12.1.tar.xz
source_filename = freetype-2.12.1.tar.xz
source_hash = 4766f20157cc4cf0cd292f80bf917f92d1c439b243ac3018debf6b9140c41a7f
wrapdb_version = 2.12.1-2
directory = freetype-2.13.2
source_url = https://download.savannah.gnu.org/releases/freetype/freetype-2.13.2.tar.xz
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/freetype2_2.13.2-1/freetype-2.13.2.tar.xz
source_filename = freetype-2.13.2.tar.xz
source_hash = 12991c4e55c506dd7f9b765933e62fd2be2e06d421505d7950a132e4f1bb484d
wrapdb_version = 2.13.2-1
[provide]
freetype2 = freetype_dep

View File

@ -1,12 +1,14 @@
[wrap-file]
directory = lua-5.4.4
source_url = https://www.lua.org/ftp/lua-5.4.4.tar.gz
source_filename = lua-5.4.4.tar.gz
source_hash = 164c7849653b80ae67bec4b7473b884bf5cc8d2dca05653475ec2ed27b9ebf61
patch_filename = lua_5.4.4-1_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/lua_5.4.4-1/get_patch
patch_hash = e61cd965c629d6543176f41a9f1cb9050edfd1566cf00ce768ff211086e40bdc
directory = lua-5.4.6
source_url = https://www.lua.org/ftp/lua-5.4.6.tar.gz
source_filename = lua-5.4.6.tar.gz
source_hash = 7d5ea1b9cb6aa0b59ca3dde1c6adcb57ef83a1ba8e5432c0ecd06bf439b3ad88
patch_filename = lua_5.4.6-3_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/lua_5.4.6-3/get_patch
patch_hash = 9b72a95422fd47f79f969d9abdb589ee95712d5512a5246f94e7e4f63d2cb7b7
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/lua_5.4.6-3/lua-5.4.6.tar.gz
wrapdb_version = 5.4.6-3
[provide]
lua-5.4 = lua_dep
lua = lua_dep

View File

@ -1,12 +1,13 @@
[wrap-file]
directory = pcre2-10.42
source_url = https://github.com/PhilipHazel/pcre2/releases/download/pcre2-10.42/pcre2-10.42.tar.bz2
source_url = https://github.com/PCRE2Project/pcre2/releases/download/pcre2-10.42/pcre2-10.42.tar.bz2
source_filename = pcre2-10.42.tar.bz2
source_hash = 8d36cd8cb6ea2a4c2bb358ff6411b0c788633a2a45dabbf1aeb4b701d1b5e840
patch_filename = pcre2_10.42-1_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/pcre2_10.42-1/get_patch
patch_hash = 06969e916dfee663c189810df57d98574f15e0754a44cd93f3f0bc7234b05d89
wrapdb_version = 10.42-1
patch_filename = pcre2_10.42-5_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/pcre2_10.42-5/get_patch
patch_hash = 7ba1730a3786c46f41735658a9884b09bc592af3840716e0ccc552e7ddf5630c
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/pcre2_10.42-5/pcre2-10.42.tar.bz2
wrapdb_version = 10.42-5
[provide]
libpcre2-8 = libpcre2_8

View File

@ -1,12 +1,15 @@
[wrap-file]
directory = SDL2-2.26.0
source_url = https://github.com/libsdl-org/SDL/releases/download/release-2.26.0/SDL2-2.26.0.tar.gz
source_filename = SDL2-2.26.0.tar.gz
source_hash = 8000d7169febce93c84b6bdf376631f8179132fd69f7015d4dadb8b9c2bdb295
patch_filename = sdl2_2.26.0-1_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/sdl2_2.26.0-1/get_patch
patch_hash = 6fcfd727d71cf7837332723518d5e47ffd64f1e7630681cf4b50e99f2bf7676f
wrapdb_version = 2.26.0-1
directory = SDL2-2.28.1
source_url = https://github.com/libsdl-org/SDL/releases/download/release-2.28.1/SDL2-2.28.1.tar.gz
source_filename = SDL2-2.28.1.tar.gz
source_hash = 4977ceba5c0054dbe6c2f114641aced43ce3bf2b41ea64b6a372d6ba129cb15d
patch_filename = sdl2_2.28.1-2_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/sdl2_2.28.1-2/get_patch
patch_hash = 2dd332226ba2a4373c6d4eb29fa915e9d5414cf7bb9fa2e4a5ef3b16a06e2736
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/sdl2_2.28.1-2/SDL2-2.28.1.tar.gz
wrapdb_version = 2.28.1-2
[provide]
sdl2 = sdl2_dep
sdl2main = sdl2main_dep
sdl2_test = sdl2_test_dep