diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a9596150..8fb880ff 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -131,3 +131,40 @@ jobs: with: name: Windows Artifacts path: ${{ env.INSTALL_NAME }}.zip + + build_windows_msvc: + name: Windows (MSVC) + runs-on: windows-2019 + strategy: + matrix: + arch: [amd64, amd64_x86] + steps: + - uses: actions/checkout@v2 + - uses: ilammy/msvc-dev-cmd@v1 + with: + arch: ${{ matrix.arch }} + - uses: actions/setup-python@v1 + with: + python-version: '3.x' + - name: Install meson and ninja + run: pip install meson ninja + - name: Set up environment variables + run: | + "INSTALL_NAME=lite-xl-$($env:GITHUB_REF -replace ".*/")-windows-msvc-${{ matrix.arch }}" >> $env:GITHUB_ENV + "INSTALL_REF=$($env:GITHUB_REF -replace ".*/")" >> $env:GITHUB_ENV + "LUA_SUBPROJECT_PATH=subprojects/lua-5.4.4" >> $env:GITHUB_ENV + - name: Configure + run: | + meson setup --wrap-mode=forcefallback build + Get-Content -Path resources/windows/001-lua-unicode.diff -Raw | patch -d $env:LUA_SUBPROJECT_PATH -p1 --forward + - name: Build + run: meson install -C build --destdir="../lite-xl" + - name: Package + run: | + Remove-Item -Recurse -Force -Path "lite-xl/lib","lite-xl/include" + Compress-Archive -Path lite-xl -DestinationPath "$env:INSTALL_NAME.zip" + - name: Upload Artifacts + uses: actions/upload-artifact@v2 + with: + name: Windows Artifacts (MSVC) + path: ${{ env.INSTALL_NAME }}.zip diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bc2f2e67..a8d97e54 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,17 +1,21 @@ name: Release on: + push: + tags: + - v[0-9]+.* + workflow_dispatch: inputs: version: description: Release Version - default: v2.1.0 + default: v2.1.1 required: true jobs: release: name: Create Release - runs-on: ubuntu-20.04 + runs-on: ubuntu-18.04 outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} version: ${{ steps.tag.outputs.version }} @@ -26,6 +30,12 @@ jobs: else echo ::set-output name=version::${GITHUB_REF/refs\/tags\//} fi + - name: Update Tag + uses: richardsimko/update-tag@v1 + with: + tag_name: ${{ steps.tag.outputs.version }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create Release id: create_release uses: softprops/action-gh-release@v1 @@ -33,14 +43,13 @@ jobs: tag_name: ${{ steps.tag.outputs.version }} name: Lite XL ${{ steps.tag.outputs.version }} draft: true - prerelease: false body_path: changelog.md generate_release_notes: true build_linux: name: Linux needs: release - runs-on: ubuntu-20.04 + runs-on: ubuntu-18.04 env: CC: gcc CXX: g++ @@ -76,6 +85,7 @@ jobs: uses: softprops/action-gh-release@v1 with: tag_name: ${{ needs.release.outputs.version }} + draft: true files: | lite-xl-${{ env.INSTALL_REF }}-linux-x86_64-portable.tar.gz lite-xl-${{ env.INSTALL_REF }}-addons-linux-x86_64-portable.tar.gz @@ -86,6 +96,9 @@ jobs: name: macOS (x86_64) needs: release runs-on: macos-11 + strategy: + matrix: + arch: [x86_64, arm64] env: CC: clang CXX: clang++ @@ -100,8 +113,8 @@ jobs: run: | echo "$HOME/.local/bin" >> "$GITHUB_PATH" echo "INSTALL_REF=${{ needs.release.outputs.version }}" >> "$GITHUB_ENV" - echo "INSTALL_NAME=lite-xl-${{ needs.release.outputs.version }}-macos-$(uname -m)" >> "$GITHUB_ENV" - echo "INSTALL_NAME_ADDONS=lite-xl-${{ needs.release.outputs.version }}-addons-macos-$(uname -m)" >> "$GITHUB_ENV" + echo "INSTALL_NAME=lite-xl-${{ needs.release.outputs.version }}-macos-${{ matrix.arch }}" >> "$GITHUB_ENV" + echo "INSTALL_NAME_ADDONS=lite-xl-${{ needs.release.outputs.version }}-addons-macos-${{ matrix.arch }}" >> "$GITHUB_ENV" - uses: actions/checkout@v2 - name: Python Setup uses: actions/setup-python@v2 @@ -112,15 +125,16 @@ jobs: - name: Build run: | bash --version - bash scripts/build.sh --bundle --debug --forcefallback --release + CROSS_ARCH=${{ matrix.arch }} bash scripts/build.sh --bundle --debug --forcefallback --release - name: Create DMG Image run: | - bash scripts/package.sh --version ${INSTALL_REF} --debug --dmg --release - bash scripts/package.sh --version ${INSTALL_REF} --debug --addons --dmg --release + CROSS_ARCH=${{ matrix.arch }} bash scripts/package.sh --version ${INSTALL_REF} --debug --dmg --release + CROSS_ARCH=${{ matrix.arch }} bash scripts/package.sh --version ${INSTALL_REF} --debug --addons --dmg --release - name: Upload Files uses: softprops/action-gh-release@v1 with: tag_name: ${{ needs.release.outputs.version }} + draft: true files: | ${{ env.INSTALL_NAME }}.dmg ${{ env.INSTALL_NAME_ADDONS }}.dmg @@ -176,6 +190,7 @@ jobs: uses: softprops/action-gh-release@v1 with: tag_name: ${{ needs.release.outputs.version }} + draft: true files: | ${{ env.INSTALL_NAME }}.zip ${{ env.INSTALL_NAME_ADDONS }}.zip diff --git a/changelog.md b/changelog.md index dbc457a5..b1d6c9d8 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,166 @@ # Changes Log -## [2.1.0] - 2022-09-25 +## [2.1.1] - 2022-12-29 + +### New Features + +* Add config.keep_newline_whitespace option + ([#1184](https://github.com/lite-xl/lite-xl/pull/1184)) + +* Add regex.find_offsets, regex.find, improve regex.match + ([#1232](https://github.com/lite-xl/lite-xl/pull/1232)) + +* Added regex.gmatch ([#1233](https://github.com/lite-xl/lite-xl/pull/1233)) + +* add touch events ([#1245](https://github.com/lite-xl/lite-xl/pull/1245)) + +### Performance Improvements + +* highlighter: autostop co-routine when not needed + ([#881](https://github.com/lite-xl/lite-xl/pull/881)) + +* core: ported regex.gsub to faster native version + ([#1233](https://github.com/lite-xl/lite-xl/pull/1233)) + +### Backward Incompatible Changes + +* For correctness, the behaviour of `regex.match` was changed to more closely + behave like `string.match`. + +* `regex.find_offsets` now provides the previous functionality of `regex.match` + with a more appropriate function name. + +* `regex.gsub` doesn't provides the indexes of matches and replacements anymore, + now it behaves more similar to `string.gsub` (the only known affected plugin + was `regexreplacepreview` which has already been adapted) + +### UI Enhancements + +* statusview: respect right padding of item tooltip + ([0373d29f](https://github.com/lite-xl/lite-xl/commit/0373d29f99f286b2fbdda5a6837ef3797c988b88)) + +* feat: encode home in statusview file path + ([#1224](https://github.com/lite-xl/lite-xl/pull/1224)) + +* autocomplete: wrap the autocomplete results around + ([#1223](https://github.com/lite-xl/lite-xl/pull/1223)) + +* feat: alert user via nagview if file cannot be saved + ([#1230](https://github.com/lite-xl/lite-xl/pull/1230)) + +* contextmenu: make divider less aggressive + ([#1228](https://github.com/lite-xl/lite-xl/pull/1228)) + +* Improve IME location updates + ([#1170](https://github.com/lite-xl/lite-xl/pull/1170)) + +* fix: move tab scroll buttons to remove spacing before 1st tab + ([#1231](https://github.com/lite-xl/lite-xl/pull/1231)) + +* Allow TreeView file operation commands when focused + ([#1256](https://github.com/lite-xl/lite-xl/pull/1256)) + +* contextmenu: adjust y positioning if less than zero + ([#1268](https://github.com/lite-xl/lite-xl/pull/1268)) + +### Fixes + +* Don't sort in Doc:get_selection_idx with an invalid index + ([b029f599](https://github.com/lite-xl/lite-xl/commit/b029f5993edb7dee5ccd2ba55faac1ec22e24609)) + +* tokenizer: remove the limit of 3 subsyntaxes depth + ([#1186](https://github.com/lite-xl/lite-xl/pull/1186)) + +* dirmonitor: give kevent a timeout so it doesn't lock forever + ([#1180](https://github.com/lite-xl/lite-xl/pull/1180)) + +* dirmonitor: fix win32 implementation name length to prevent ub + ([5ab8dc0](https://github.com/lite-xl/lite-xl/commit/5ab8dc027502146dd947b3d2c7544ba096a3881b)) + +* Make linewrapping plugin recompute breaks before scrolling + ([#1190](https://github.com/lite-xl/lite-xl/pull/1190)) + +* Add missing get_exe_filename() implementation for FreeBSD + ([#1198](https://github.com/lite-xl/lite-xl/pull/1198)) + +* (Windows) Load fonts with UTF-8 filenames + ([#1201](https://github.com/lite-xl/lite-xl/pull/1201)) + +* Use subsyntax info to toggle comments + ([#1202](https://github.com/lite-xl/lite-xl/pull/1202)) + +* Pass the currently selected item to CommandView validation + ([#1203](https://github.com/lite-xl/lite-xl/pull/1203)) + +* Windows font loading hotfix + ([#1205](https://github.com/lite-xl/lite-xl/pull/1205)) + +* better error messages for checkcolor + ([#1211](https://github.com/lite-xl/lite-xl/pull/1211)) + +* Fix native plugins not reloading upon core:restart + ([#1219](https://github.com/lite-xl/lite-xl/pull/1219)) + +* Converted from bytes to characters, as this is what windows is expecting + ([5ab8dc02](https://github.com/lite-xl/lite-xl/commit/5ab8dc027502146dd947b3d2c7544ba096a3881b)) + +* Fix some syntax errors ([#1243](https://github.com/lite-xl/lite-xl/pull/1243)) + +* toolbarview: Remove tooltip when hidden + ([#1251](https://github.com/lite-xl/lite-xl/pull/1251)) + +* detectindent: Limit subsyntax depth + ([#1253](https://github.com/lite-xl/lite-xl/pull/1253)) + +* Use Lua string length instead of relying on strlen (#1262) + ([#1262](https://github.com/lite-xl/lite-xl/pull/1262)) + +* dirmonitor: fix high cpu usage + ([#1271](https://github.com/lite-xl/lite-xl/pull/1271)), + ([#1274](https://github.com/lite-xl/lite-xl/pull/1274)) + +* Fix popping subsyntaxes that end consecutively + ([#1246](https://github.com/lite-xl/lite-xl/pull/1246)) + +* Fix userdata APIs for Lua 5.4 in native plugin interface + ([#1188](https://github.com/lite-xl/lite-xl/pull/1188)) + +* Fix horizontal scroll with touchpad on MacOS + ([74349f8e](https://github.com/lite-xl/lite-xl/commit/74349f8e566ec31acd9a831a060b677d706ae4e8)) + +### Other Changes + +* (Windows) MSVC Support ([#1199](https://github.com/lite-xl/lite-xl/pull/1199)) + +* meson: updated all subproject wraps + ([#1214](https://github.com/lite-xl/lite-xl/pull/1214)) + +* set arch tuple in meson ([#1254](https://github.com/lite-xl/lite-xl/pull/1254)) + +* update documentation for system + ([#1210](https://github.com/lite-xl/lite-xl/pull/1210)) + +* docs api: added dirmonitor + ([7bb86e16](https://github.com/lite-xl/lite-xl/commit/7bb86e16f291256a99d2e87beb77de890cfaf0fe)) + +* trimwhitespace: expose functionality and extra features + ([#1238](https://github.com/lite-xl/lite-xl/pull/1238)) + +* plugins projectsearch: expose its functionality + ([#1235](https://github.com/lite-xl/lite-xl/pull/1235)) + +* Simplify SDL message boxes + ([#1249](https://github.com/lite-xl/lite-xl/pull/1249)) + +* Add example settings to _overwrite_ an existing key binding + ([#1270](https://github.com/lite-xl/lite-xl/pull/1270)) + +* Fix two typos in data/init.lua + ([#1272](https://github.com/lite-xl/lite-xl/pull/1272)) + +* Updated meson wraps to latest (SDL v2.26, PCRE2 v10.42) + +## [2.1.0] - 2022-11-01 ### New Features * Make distinction between @@ -124,6 +284,12 @@ * Added in ability to have init.so as a require for cpath. ([#1126](https://github.com/lite-xl/lite-xl/pull/1126)) +* Added system.raise_window() ([#1131](https://github.com/lite-xl/lite-xl/pull/1131)) + +* Initial horizontal scrollbar support ([#1124](https://github.com/lite-xl/lite-xl/pull/1124)) + +* IME support ([#991](https://github.com/lite-xl/lite-xl/pull/991)) + ### Performance Improvements * [Load space metrics only when creating font](https://github.com/lite-xl/lite-xl/pull/1032) @@ -327,11 +493,19 @@ * [Fix memory leak](https://github.com/lite-xl/lite-xl/pull/1039) and wrong check in font_retrieve -* Many, many, many more changes that are too numerous to list. - * CommandView: do not change caret size with config.line_height ([#1080](https://github.com/lite-xl/lite-xl/pull/1080)) +* Fixed process layer argument quoting; allows for strings with spaces + ([#1132](https://github.com/lite-xl/lite-xl/pull/1132)) + +* Draw lite-xl icon in TitleView ([#1143](https://github.com/lite-xl/lite-xl/pull/1143)) + +* Add parameter validation to checkcolor and f_font_group + ([#1145](https://github.com/lite-xl/lite-xl/pull/1145)) + +* Many, many, many more changes that are too numerous to list. + ## [2.0.5] - 2022-01-29 Revamp the project's user module so that modifications are immediately applied. @@ -830,6 +1004,7 @@ A new global variable `USERDIR` is exposed to point to the user's directory. - subpixel font rendering with gamma correction +[2.1.1]: https://github.com/lite-xl/lite-xl/releases/tag/v2.1.1 [2.1.0]: https://github.com/lite-xl/lite-xl/releases/tag/v2.1.0 [2.0.5]: https://github.com/lite-xl/lite-xl/releases/tag/v2.0.5 [2.0.4]: https://github.com/lite-xl/lite-xl/releases/tag/v2.0.4 diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index 365c0d19..6d32219a 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -3,12 +3,9 @@ local command = require "core.command" local common = require "core.common" local config = require "core.config" local translate = require "core.doc.translate" +local style = require "core.style" local DocView = require "core.docview" - - -local function dv() - return core.active_view -end +local tokenizer = require "core.tokenizer" local function doc() @@ -40,9 +37,24 @@ local function save(filename) filename = core.normalize_to_project_dir(filename) abs_filename = core.project_absolute_path(filename) end - doc():save(filename, abs_filename) - local saved_filename = doc().filename - core.log("Saved \"%s\"", saved_filename) + local ok, err = pcall(doc().save, doc(), filename, abs_filename) + if ok then + local saved_filename = doc().filename + core.log("Saved \"%s\"", saved_filename) + else + core.error(err) + core.nag_view:show("Saving failed", string.format("Could not save \"%s\" do you want to save to another location?", doc().filename), { + { font = style.font, text = "No", default_no = true }, + { font = style.font, text = "Yes" , default_yes = true } + }, function(item) + if item.text == "Yes" then + core.add_thread(function() + -- we need to run this in a thread because of the odd way the nagview is. + command.perform("doc:save-as") + end) + end + end) + end end local function cut_or_copy(delete) @@ -59,8 +71,9 @@ local function cut_or_copy(delete) doc():delete_to_cursor(idx, 0) end else -- Cut/copy whole line - text = doc().lines[line1] - full_text = full_text == "" and text or (full_text .. text) + -- Remove newline from the text. It will be added as needed on paste. + text = string.sub(doc().lines[line1], 1, -2) + full_text = full_text == "" and text or (full_text .. text .. "\n") core.cursor_clipboard_whole_line[idx] = true if delete then if line1 < #doc().lines then @@ -85,7 +98,15 @@ local function split_cursor(direction) table.insert(new_cursors, { line1 + direction, col1 }) end end - for i,v in ipairs(new_cursors) do doc():add_selection(v[1], v[2]) end + -- add selections in the order that will leave the "last" added one as doc.last_selection + local start, stop = 1, #new_cursors + if direction < 0 then + start, stop = #new_cursors, 1 + end + for i = start, stop, direction do + local v = new_cursors[i] + doc():add_selection(v[1], v[2]) + end core.blink_reset() end @@ -177,10 +198,30 @@ local function block_comment(comment, line1, col1, line2, col2) end end +local function insert_paste(doc, value, whole_line, idx) + if whole_line then + local line1, col1 = doc:get_selection_idx(idx) + doc:insert(line1, 1, value:gsub("\r", "").."\n") + -- Because we're inserting at the start of the line, + -- if the cursor is in the middle of the line + -- it gets carried to the next line along with the old text. + -- If it's at the start of the line it doesn't get carried, + -- so we move it of as many characters as we're adding. + if col1 == 1 then + doc:move_to_cursor(idx, #value+1) + end + else + doc:text_input(value:gsub("\r", ""), idx) + end +end + local commands = { ["doc:select-none"] = function(dv) - local line, col = dv.doc:get_selection() - dv.doc:set_selection(line, col) + local l1, c1 = dv.doc:get_selection_idx(dv.doc.last_selection) + if not l1 then + l1, c1 = dv.doc:get_selection_idx(1) + end + dv.doc:set_selection(l1, c1) end, ["doc:cut"] = function() @@ -202,27 +243,51 @@ local commands = { ["doc:paste"] = function(dv) local clipboard = system.get_clipboard() -- If the clipboard has changed since our last look, use that instead - local external_paste = core.cursor_clipboard["full"] ~= clipboard - if external_paste then + if core.cursor_clipboard["full"] ~= clipboard then core.cursor_clipboard = {} core.cursor_clipboard_whole_line = {} - end - local value, whole_line - for idx, line1, col1, line2, col2 in dv.doc:get_selections() do - if #core.cursor_clipboard_whole_line == (#dv.doc.selections/4) then - value = core.cursor_clipboard[idx] - whole_line = core.cursor_clipboard_whole_line[idx] == true - else - value = clipboard - whole_line = not external_paste and clipboard:find("\n") ~= nil + for idx in dv.doc:get_selections() do + insert_paste(dv.doc, clipboard, false, idx) end - if whole_line then - dv.doc:insert(line1, 1, value:gsub("\r", "")) - if col1 == 1 then - dv.doc:move_to_cursor(idx, #value) + return + end + -- Use internal clipboard(s) + -- If there are mixed whole lines and normal lines, consider them all as normal + local only_whole_lines = true + for _,whole_line in pairs(core.cursor_clipboard_whole_line) do + if not whole_line then + only_whole_lines = false + break + end + end + if #core.cursor_clipboard_whole_line == (#dv.doc.selections/4) then + -- If we have the same number of clipboards and selections, + -- paste each clipboard into its corresponding selection + for idx in dv.doc:get_selections() do + insert_paste(dv.doc, core.cursor_clipboard[idx], only_whole_lines, idx) + end + else + -- Paste every clipboard and add a selection at the end of each one + local new_selections = {} + for idx in dv.doc:get_selections() do + for cb_idx in ipairs(core.cursor_clipboard_whole_line) do + insert_paste(dv.doc, core.cursor_clipboard[cb_idx], only_whole_lines, idx) + if not only_whole_lines then + table.insert(new_selections, {dv.doc:get_selection_idx(idx)}) + end + end + if only_whole_lines then + table.insert(new_selections, {dv.doc:get_selection_idx(idx)}) + end + end + local first = true + for _,selection in pairs(new_selections) do + if first then + dv.doc:set_selection(table.unpack(selection)) + first = false + else + dv.doc:add_selection(table.unpack(selection)) end - else - dv.doc:text_input(value:gsub("\r", ""), idx) end end end, @@ -234,7 +299,7 @@ local commands = { indent = indent:sub(#indent + 2 - col) end -- Remove current line if it contains only whitespace - if dv.doc.lines[line]:match("^%s+$") then + if not config.keep_newline_whitespace and dv.doc.lines[line]:match("^%s+$") then dv.doc:remove(line, 1, line, math.huge) end dv.doc:text_input("\n" .. indent, idx) @@ -380,15 +445,28 @@ local commands = { end, ["doc:toggle-block-comments"] = function(dv) - local comment = dv.doc.syntax.block_comment - if not comment then - if dv.doc.syntax.comment then - command.perform "doc:toggle-line-comments" - end - return - end - for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do + local current_syntax = dv.doc.syntax + if line1 > 1 then + -- Use the previous line state, as it will be the state + -- of the beginning of the current line + local state = dv.doc.highlighter:get_line(line1 - 1).state + local syntaxes = tokenizer.extract_subsyntaxes(dv.doc.syntax, state) + -- Go through all the syntaxes until the first with `block_comment` defined + for _, s in pairs(syntaxes) do + if s.block_comment then + current_syntax = s + break + end + end + end + local comment = current_syntax.block_comment + if not comment then + if dv.doc.syntax.comment then + command.perform "doc:toggle-line-comments" + end + return + end -- if nothing is selected, toggle the whole line if line1 == line2 and col1 == col2 then col1 = 1 @@ -399,9 +477,23 @@ local commands = { end, ["doc:toggle-line-comments"] = function(dv) - local comment = dv.doc.syntax.comment or dv.doc.syntax.block_comment - if comment then - for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do + for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do + local current_syntax = dv.doc.syntax + if line1 > 1 then + -- Use the previous line state, as it will be the state + -- of the beginning of the current line + local state = dv.doc.highlighter:get_line(line1 - 1).state + local syntaxes = tokenizer.extract_subsyntaxes(dv.doc.syntax, state) + -- Go through all the syntaxes until the first with comments defined + for _, s in pairs(syntaxes) do + if s.comment or s.block_comment then + current_syntax = s + break + end + end + end + local comment = current_syntax.comment or current_syntax.block_comment + if comment then dv.doc:set_selections(idx, line_comment(comment, line1, col1, line2, col2)) end end @@ -538,7 +630,7 @@ local commands = { } command.add(function(x, y) - if x == nil or y == nil or not core.active_view:is(DocView) then return false end + if x == nil or y == nil or not core.active_view:extends(DocView) then return false end local dv = core.active_view local x1,y1,x2,y2 = dv.position.x, dv.position.y, dv.position.x + dv.size.x, dv.position.y + dv.size.y return x >= x1 + dv:get_gutter_width() and x < x2 and y >= y1 and y < y2, dv, x, y diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua index 3966dbf4..4a456aae 100644 --- a/data/core/commands/findreplace.lua +++ b/data/core/commands/findreplace.lua @@ -216,7 +216,7 @@ command.add("core.docview!", { return text:gsub(old:gsub("%W", "%%%1"), new:gsub("%%", "%%%%"), nil) end local result, matches = regex.gsub(regex.compile(old, "m"), text, new) - return result, #matches + return result, matches end) end, diff --git a/data/core/commands/root.lua b/data/core/commands/root.lua index deea858e..61e3890b 100644 --- a/data/core/commands/root.lua +++ b/data/core/commands/root.lua @@ -123,5 +123,13 @@ command.add(nil, { return true end return false + end, + ["root:horizontal-scroll"] = function(delta) + local view = (core.root_view.overlapping_node and core.root_view.overlapping_node.active_view) or core.active_view + if view and view.scrollable then + view.scroll.to.x = view.scroll.to.x + delta * -config.mouse_wheel_scroll + return true + end + return false end }) diff --git a/data/core/commandview.lua b/data/core/commandview.lua index a77db961..1f388678 100644 --- a/data/core/commandview.lua +++ b/data/core/commandview.lua @@ -88,6 +88,10 @@ function CommandView:get_scrollable_size() return 0 end +function CommandView:get_h_scrollable_size() + return 0 +end + function CommandView:scroll_to_make_visible() -- no-op function to disable this functionality @@ -155,7 +159,7 @@ end function CommandView:submit() local suggestion = self.suggestions[self.suggestion_idx] local text = self:get_text() - if self.state.validate(text) then + if self.state.validate(text, suggestion) then local submit = self.state.submit self:exit(true) submit(text, suggestion) diff --git a/data/core/config.lua b/data/core/config.lua index efbe1f1b..ecdcfdb5 100644 --- a/data/core/config.lua +++ b/data/core/config.lua @@ -6,8 +6,19 @@ config.message_timeout = 5 config.mouse_wheel_scroll = 50 * SCALE config.animate_drag_scroll = false config.scroll_past_end = true +---@type "expanded" | "contracted" | false @Force the scrollbar status of the DocView +config.force_scrollbar_status = false config.file_size_limit = 10 -config.ignore_files = { "^%." } +config.ignore_files = { + -- folders + "^%.svn/", "^%.git/", "^%.hg/", "^CVS/", "^%.Trash/", "^%.Trash%-.*/", + "^node_modules/", "^%.cache/", "^__pycache__/", + -- files + "%.pyc$", "%.pyo$", "%.exe$", "%.dll$", "%.obj$", "%.o$", + "%.a$", "%.lib$", "%.so$", "%.dylib$", "%.ncb$", "%.sdf$", + "%.suo$", "%.pdb$", "%.idb$", "%.class$", "%.psd$", "%.db$", + "^desktop%.ini$", "^%.DS_Store$", "^%.directory$", +} config.symbol_pattern = "[%a_][%w_]*" config.non_word_chars = " \t\n/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-" config.undo_merge_timeout = 0.3 @@ -19,6 +30,7 @@ config.highlight_current_line = true config.line_height = 1.2 config.indent_size = 2 config.tab_type = "soft" +config.keep_newline_whitespace = false config.line_limit = 80 config.max_project_files = 2000 config.transitions = true @@ -47,8 +59,9 @@ config.plugins = {} -- Allow you to set plugin configs even if we haven't seen the plugin before. setmetatable(config.plugins, { __index = function(t, k) - if rawget(t, k) == nil then rawset(t, k, {}) end - return rawget(t, k) + local v = rawget(t, k) + if v == true or v == nil then v = {} rawset(t, k, v) end + return v end }) diff --git a/data/core/contextmenu.lua b/data/core/contextmenu.lua index 380fb634..4b466907 100644 --- a/data/core/contextmenu.lua +++ b/data/core/contextmenu.lua @@ -9,6 +9,7 @@ local View = require "core.view" local border_width = 1 local divider_width = 1 +local divider_padding = 5 local DIVIDER = {} ---@class core.contextmenu : core.object @@ -29,7 +30,7 @@ local function get_item_size(item) local lw, lh if item == DIVIDER then lw = 0 - lh = divider_width + lh = divider_width + divider_padding * SCALE * 2 else lw = style.font:get_width(item.text) if item.info then @@ -82,12 +83,8 @@ function ContextMenu:show(x, y) local w, h = self.items.width, self.items.height -- by default the box is opened on the right and below - if x + w >= core.root_view.size.x then - x = x - w - end - if y + h >= core.root_view.size.y then - y = y - h - end + x = common.clamp(x, 0, core.root_view.size.x - w - style.padding.x) + y = common.clamp(y, 0, core.root_view.size.y - h) self.position.x, self.position.y = x, y self.show_context_menu = true @@ -224,7 +221,7 @@ function ContextMenu:draw_context_menu() for i, item, x, y, w, h in self:each_item() do if item == DIVIDER then - renderer.draw_rect(x, y, w, h, style.caret) + renderer.draw_rect(x, y + divider_padding * SCALE, w, divider_width, style.divider) else if i == self.selected then renderer.draw_rect(x, y, w, h, style.selection) diff --git a/data/core/dirwatch.lua b/data/core/dirwatch.lua index 5553047d..e3b6d61c 100644 --- a/data/core/dirwatch.lua +++ b/data/core/dirwatch.lua @@ -2,7 +2,7 @@ local common = require "core.common" local config = require "core.config" local dirwatch = {} -function dirwatch:__index(idx) +function dirwatch:__index(idx) local value = rawget(self, idx) if value ~= nil then return value end return dirwatch[idx] @@ -14,8 +14,8 @@ function dirwatch.new() watched = {}, reverse_watched = {}, monitor = dirmonitor.new(), - windows_watch_top = nil, - windows_watch_count = 0 + single_watch_top = nil, + single_watch_count = 0 } setmetatable(t, dirwatch) return t @@ -38,23 +38,23 @@ function dirwatch:watch(directory, bool) local info = system.get_file_info(directory) if not info then return end if not self.watched[directory] and not self.scanned[directory] then - if PLATFORM == "Windows" then + if self.monitor:mode() == "single" then if info.type ~= "dir" then return self:scan(directory) end - if not self.windows_watch_top or directory:find(self.windows_watch_top, 1, true) ~= 1 then + if not self.single_watch_top or directory:find(self.single_watch_top, 1, true) ~= 1 then -- Get the highest level of directory that is common to this directory, and the original. local target = directory - while self.windows_watch_top and self.windows_watch_top:find(target, 1, true) ~= 1 do + while self.single_watch_top and self.single_watch_top:find(target, 1, true) ~= 1 do target = common.dirname(target) end - if target ~= self.windows_watch_top then + if target ~= self.single_watch_top then local value = self.monitor:watch(target) if value and value < 0 then return self:scan(directory) end - self.windows_watch_top = target + self.single_watch_top = target end end - self.windows_watch_count = self.windows_watch_count + 1 + self.single_watch_count = self.single_watch_count + 1 self.watched[directory] = true else local value = self.monitor:watch(directory) @@ -72,13 +72,13 @@ end -- this should be an absolute path function dirwatch:unwatch(directory) if self.watched[directory] then - if PLATFORM ~= "Windows" then + if self.monitor:mode() == "multiple" then self.monitor:unwatch(self.watched[directory]) self.reverse_watched[directory] = nil else - self.windows_watch_count = self.windows_watch_count - 1 - if self.windows_watch_count == 0 then - self.windows_watch_top = nil + self.single_watch_count = self.single_watch_count - 1 + if self.single_watch_count == 0 then + self.single_watch_top = nil self.monitor:unwatch(directory) end end @@ -93,8 +93,12 @@ function dirwatch:check(change_callback, scan_time, wait_time) local had_change = false self.monitor:check(function(id) had_change = true - if PLATFORM == "Windows" then - change_callback(common.dirname(self.windows_watch_top .. PATHSEP .. id)) + if self.monitor:mode() == "single" then + local path = common.dirname(id) + if not string.match(id, "^/") and not string.match(id, "^%a:[/\\]") then + path = common.dirname(self.single_watch_top .. PATHSEP .. id) + end + change_callback(path) elseif self.reverse_watched[id] then change_callback(self.reverse_watched[id]) end @@ -162,7 +166,7 @@ end local function compare_file(a, b) - return a.filename < b.filename + return system.path_compare(a.filename, a.type, b.filename, b.type) end diff --git a/data/core/doc/highlighter.lua b/data/core/doc/highlighter.lua index 888c82aa..0cddb2c3 100644 --- a/data/core/doc/highlighter.lua +++ b/data/core/doc/highlighter.lua @@ -10,42 +10,49 @@ local Highlighter = Object:extend() function Highlighter:new(doc) self.doc = doc + self.running = false self:reset() +end - -- init incremental syntax highlighting +-- init incremental syntax highlighting +function Highlighter:start() + if self.running then return end + self.running = true core.add_thread(function() - while true do - if self.first_invalid_line > self.max_wanted_line then - self.max_wanted_line = 0 - coroutine.yield(1 / config.fps) - - else - local max = math.min(self.first_invalid_line + 40, self.max_wanted_line) - - local retokenized_from - for i = self.first_invalid_line, max do - local state = (i > 1) and self.lines[i - 1].state - local line = self.lines[i] - if not (line and line.init_state == state and line.text == self.doc.lines[i]) then - retokenized_from = retokenized_from or i - self.lines[i] = self:tokenize_line(i, state) - elseif retokenized_from then - self:update_notify(retokenized_from, i - retokenized_from - 1) - retokenized_from = nil - end + while self.first_invalid_line < self.max_wanted_line do + local max = math.min(self.first_invalid_line + 40, self.max_wanted_line) + local retokenized_from + for i = self.first_invalid_line, max do + local state = (i > 1) and self.lines[i - 1].state + local line = self.lines[i] + if not (line and line.init_state == state and line.text == self.doc.lines[i]) then + retokenized_from = retokenized_from or i + self.lines[i] = self:tokenize_line(i, state) + elseif retokenized_from then + self:update_notify(retokenized_from, i - retokenized_from - 1) + retokenized_from = nil end - if retokenized_from then - self:update_notify(retokenized_from, max - retokenized_from) - end - - self.first_invalid_line = max + 1 - core.redraw = true - coroutine.yield() end + if retokenized_from then + self:update_notify(retokenized_from, max - retokenized_from) + end + + self.first_invalid_line = max + 1 + core.redraw = true + coroutine.yield() end + self.max_wanted_line = 0 + self.running = false end, self) end +local function set_max_wanted_lines(self, amount) + self.max_wanted_line = amount + if self.first_invalid_line < self.max_wanted_line then + self:start() + end +end + function Highlighter:reset() self.lines = {} @@ -62,7 +69,7 @@ end function Highlighter:invalidate(idx) self.first_invalid_line = math.min(self.first_invalid_line, idx) - self.max_wanted_line = math.min(self.max_wanted_line, #self.doc.lines) + set_max_wanted_lines(self, math.min(self.max_wanted_line, #self.doc.lines)) end function Highlighter:insert_notify(line, n) @@ -101,7 +108,7 @@ function Highlighter:get_line(idx) self.lines[idx] = line self:update_notify(idx, 0) end - self.max_wanted_line = math.max(self.max_wanted_line, idx) + set_max_wanted_lines(self, math.max(self.max_wanted_line, idx)) return line end diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index 4136575d..26fc2a52 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -33,6 +33,7 @@ end function Doc:reset() self.lines = { "\n" } self.selections = { 1, 1, 1, 1 } + self.last_selection = 1 self.undo_stack = { idx = 1 } self.redo_stack = { idx = 1 } self.clean_change_id = 1 @@ -141,15 +142,39 @@ function Doc:get_change_id() return self.undo_stack.idx end +local function sort_positions(line1, col1, line2, col2) + if line1 > line2 or line1 == line2 and col1 > col2 then + return line2, col2, line1, col1, true + end + return line1, col1, line2, col2, false +end + -- Cursor section. Cursor indices are *only* valid during a get_selections() call. -- Cursors will always be iterated in order from top to bottom. Through normal operation -- curors can never swap positions; only merge or split, or change their position in cursor -- order. function Doc:get_selection(sort) - local idx, line1, col1, line2, col2, swap = self:get_selections(sort)({ self.selections, sort }, 0) + local line1, col1, line2, col2, swap = self:get_selection_idx(self.last_selection, sort) + if not line1 then + line1, col1, line2, col2, swap = self:get_selection_idx(1, sort) + end return line1, col1, line2, col2, swap end + +---Get the selection specified by `idx` +---@param idx integer @the index of the selection to retrieve +---@param sort? boolean @whether to sort the selection returned +---@return integer,integer,integer,integer,boolean? @line1, col1, line2, col2, was the selection sorted +function Doc:get_selection_idx(idx, sort) + local line1, col1, line2, col2 = self.selections[idx*4-3], self.selections[idx*4-2], self.selections[idx*4-1], self.selections[idx*4] + if line1 and sort then + return sort_positions(line1, col1, line2, col2) + else + return line1, col1, line2, col2 + end +end + function Doc:get_selection_text(limit) limit = limit or math.huge local result = {} @@ -181,13 +206,6 @@ function Doc:sanitize_selection() end end -local function sort_positions(line1, col1, line2, col2) - if line1 > line2 or line1 == line2 and col1 > col2 then - return line2, col2, line1, col1, true - end - return line1, col1, line2, col2, false -end - function Doc:set_selections(idx, line1, col1, line2, col2, swap, rm) assert(not line2 == not col2, "expected 3 or 5 arguments") if swap then line1, col1, line2, col2 = line2, col2, line1, col1 end @@ -206,10 +224,14 @@ function Doc:add_selection(line1, col1, line2, col2, swap) end end self:set_selections(target, line1, col1, line2, col2, swap, 0) + self.last_selection = target end function Doc:remove_selection(idx) + if self.last_selection >= idx then + self.last_selection = self.last_selection - 1 + end common.splice(self.selections, (idx - 1) * 4 + 1, 4) end @@ -217,6 +239,7 @@ end function Doc:set_selection(line1, col1, line2, col2, swap) self.selections = {} self:set_selections(1, line1, col1, line2, col2, swap) + self.last_selection = 1 end function Doc:merge_cursors(idx) @@ -225,6 +248,9 @@ function Doc:merge_cursors(idx) if self.selections[i] == self.selections[j] and self.selections[i+1] == self.selections[j+1] then common.splice(self.selections, i, 4) + if self.last_selection >= (i+3)/4 then + self.last_selection = self.last_selection - 1 + end break end end @@ -456,6 +482,18 @@ function Doc:text_input(text, idx) end end + +function Doc:ime_text_editing(text, start, length, idx) + for sidx, line1, col1, line2, col2 in self:get_selections(true, idx or true) do + if line1 ~= line2 or col1 ~= col2 then + self:delete_to_cursor(sidx) + end + self:insert(line1, col1, text) + self:set_selections(sidx, line1, col1 + #text, line1, col1) + end +end + + function Doc:replace_cursor(idx, line1, col1, line2, col2, fn) local old_text = self:get_text(line1, col1, line2, col2) local new_text, res = fn(old_text) diff --git a/data/core/docview.lua b/data/core/docview.lua index f4270e9f..6b7913c8 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -4,6 +4,7 @@ local config = require "core.config" local style = require "core.style" local keymap = require "core.keymap" local translate = require "core.doc.translate" +local ime = require "core.ime" local View = require "core.view" ---@class core.docview : core.view @@ -60,6 +61,11 @@ function DocView:new(doc) self.doc = assert(doc) self.font = "code_font" self.last_x_offset = {} + self.ime_selection = { from = 0, size = 0 } + self.ime_status = false + self.hovering_gutter = false + self.v_scrollbar:set_forced_status(config.force_scrollbar_status) + self.h_scrollbar:set_forced_status(config.force_scrollbar_status) end @@ -111,6 +117,10 @@ function DocView:get_scrollable_size() return self:get_line_height() * (#self.doc.lines - 1) + self.size.y end +function DocView:get_h_scrollable_size() + return math.huge +end + function DocView:get_font() return style[self.font] @@ -239,8 +249,14 @@ end function DocView:on_mouse_moved(x, y, ...) DocView.super.on_mouse_moved(self, x, y, ...) - if self.hovered_scrollbar_track or self.dragging_scrollbar then + self.hovering_gutter = false + local gw = self:get_gutter_width() + + if self:scrollbar_hovering() or self:scrollbar_dragging() then self.cursor = "arrow" + elseif gw > 0 and x >= self.position.x and x <= (self.position.x + gw) then + self.cursor = "arrow" + self.hovering_gutter = true else self.cursor = "ibeam" end @@ -282,6 +298,29 @@ function DocView:mouse_selection(doc, snap_type, line1, col1, line2, col2) end +function DocView:on_mouse_pressed(button, x, y, clicks) + if button ~= "left" or not self.hovering_gutter then + return DocView.super.on_mouse_pressed(self, button, x, y, clicks) + end + local line = self:resolve_screen_position(x, y) + if keymap.modkeys["shift"] then + local sline, scol, sline2, scol2 = self.doc:get_selection(true) + if line > sline then + self.doc:set_selection(sline, 1, line, #self.doc.lines[line]) + else + self.doc:set_selection(line, 1, sline2, #self.doc.lines[sline2]) + end + else + if clicks == 1 then + self.doc:set_selection(line, 1, line, 1) + elseif clicks == 2 then + self.doc:set_selection(line, 1, line, #self.doc.lines[line]) + end + end + return true +end + + function DocView:on_mouse_released(...) DocView.super.on_mouse_released(self, ...) self.mouse_selecting = nil @@ -292,13 +331,53 @@ function DocView:on_text_input(text) self.doc:text_input(text) end +function DocView:on_ime_text_editing(text, start, length) + self.doc:ime_text_editing(text, start, length) + self.ime_status = #text > 0 + self.ime_selection.from = start + self.ime_selection.size = length + + -- Set the composition bounding box that the system IME + -- will consider when drawing its interface + local line1, col1, line2, col2 = self.doc:get_selection(true) + local col = math.min(col1, col2) + self:update_ime_location() + self:scroll_to_make_visible(line1, col + start) +end + +---Update the composition bounding box that the system IME +---will consider when drawing its interface +function DocView:update_ime_location() + if not self.ime_status then return end + + local line1, col1, line2, col2 = self.doc:get_selection(true) + local x, y = self:get_line_screen_position(line1) + local h = self:get_line_height() + local col = math.min(col1, col2) + + local x1, x2 = 0, 0 + + if self.ime_selection.size > 0 then + -- focus on a part of the text + local from = col + self.ime_selection.from + local to = from + self.ime_selection.size + x1 = self:get_col_x_offset(line1, from) + x2 = self:get_col_x_offset(line1, to) + else + -- focus the whole text + x1 = self:get_col_x_offset(line1, col1) + x2 = self:get_col_x_offset(line2, col2) + end + + ime.set_location(x + x1, y, x2 - x1, h) +end function DocView:update() -- scroll to make caret visible and reset blink timer if it moved local line1, col1, line2, col2 = self.doc:get_selection() if (line1 ~= self.last_line1 or col1 ~= self.last_col1 or line2 ~= self.last_line2 or col2 ~= self.last_col2) and self.size.x > 0 then - if core.active_view == self then + if core.active_view == self and not ime.editing then self:scroll_to_make_visible(line1, col1) end core.blink_reset() @@ -316,6 +395,8 @@ function DocView:update() core.blink_timer = tb end + self:update_ime_location() + DocView.super.update(self) end @@ -329,9 +410,17 @@ end function DocView:draw_line_text(line, x, y) local default_font = self:get_font() local tx, ty = x, y + self:get_line_text_y_offset() - for _, type, text in self.doc.highlighter:each_token(line) do + local last_token = nil + local tokens = self.doc.highlighter:get_line(line).tokens + local tokens_count = #tokens + if string.sub(tokens[tokens_count], -1) == "\n" then + last_token = tokens_count - 1 + end + for tidx, type, text in self.doc.highlighter:each_token(line) do local color = style.syntax[type] local font = style.syntax_fonts[type] or default_font + -- do not render newline, fixes issue #1164 + if tidx == last_token then text = text:sub(1, -2) end tx = renderer.draw_text(font, text, tx, ty, color) end return self:get_line_height() @@ -399,17 +488,45 @@ function DocView:draw_line_gutter(line, x, y, width) end +function DocView:draw_ime_decoration(line1, col1, line2, col2) + local x, y = self:get_line_screen_position(line1) + local line_size = math.max(1, SCALE) + local lh = self:get_line_height() + + -- Draw IME underline + local x1 = self:get_col_x_offset(line1, col1) + local x2 = self:get_col_x_offset(line2, col2) + renderer.draw_rect(x + math.min(x1, x2), y + lh - line_size, math.abs(x1 - x2), line_size, style.text) + + -- Draw IME selection + local col = math.min(col1, col2) + local from = col + self.ime_selection.from + local to = from + self.ime_selection.size + x1 = self:get_col_x_offset(line1, from) + if from ~= to then + x2 = self:get_col_x_offset(line1, to) + line_size = style.caret_width + renderer.draw_rect(x + math.min(x1, x2), y + lh - line_size, math.abs(x1 - x2), line_size, style.caret) + end + self:draw_caret(x + x1, y) +end + + function DocView:draw_overlay() if core.active_view == self then local minline, maxline = self:get_visible_line_range() -- draw caret if it overlaps this line local T = config.blink_period - for _, line, col in self.doc:get_selections() do - if line >= minline and line <= maxline + for _, line1, col1, line2, col2 in self.doc:get_selections() do + if line1 >= minline and line1 <= maxline and system.window_has_focus() then - if config.disable_blink - or (core.blink_timer - core.blink_start) % T < T / 2 then - self:draw_caret(self:get_line_screen_position(line, col)) + if ime.editing then + self:draw_ime_decoration(line1, col1, line2, col2) + else + if config.disable_blink + or (core.blink_timer - core.blink_start) % T < T / 2 then + self:draw_caret(self:get_line_screen_position(line1, col1)) + end end end end diff --git a/data/core/emptyview.lua b/data/core/emptyview.lua index 0d0a929e..2152da3b 100644 --- a/data/core/emptyview.lua +++ b/data/core/emptyview.lua @@ -7,9 +7,15 @@ local View = require "core.view" local EmptyView = View:extend() local function draw_text(x, y, color) + local lines = { + { fmt = "%s to run a command", cmd = "core:find-command" }, + { fmt = "%s to open a file from the project", cmd = "core:find-file" }, + { fmt = "%s to change project folder", cmd = "core:change-project-folder" }, + { fmt = "%s to open a project folder", cmd = "core:open-project-folder" }, + } local th = style.big_font:get_height() local dh = 2 * th + style.padding.y * 2 - local x1, y1 = x, y + (dh - th) / 2 + local x1, y1 = x, y + ((dh - th) / #lines) local xv = x1 local title = "Lite XL" local version = "version " .. VERSION @@ -24,12 +30,6 @@ local function draw_text(x, y, color) renderer.draw_text(style.font, version, xv, y1 + th, color) x = x + style.padding.x renderer.draw_rect(x, y, math.ceil(1 * SCALE), dh, color) - local lines = { - { fmt = "%s to run a command", cmd = "core:find-command" }, - { fmt = "%s to open a file from the project", cmd = "core:find-file" }, - { fmt = "%s to change project folder", cmd = "core:change-project-folder" }, - { fmt = "%s to open a project folder", cmd = "core:open-project-folder" }, - } th = style.font:get_height() y = y + (dh - (th + style.padding.y) * #lines) / 2 local w = 0 diff --git a/data/core/ime.lua b/data/core/ime.lua new file mode 100644 index 00000000..85803f17 --- /dev/null +++ b/data/core/ime.lua @@ -0,0 +1,92 @@ +local core = require "core" + +local ime = { } + +function ime.reset() + ime.editing = false + ime.last_location = { x = 0, y = 0, w = 0, h = 0 } +end + +---Convert from utf-8 offset and length (from SDL) to byte offsets +---@param text string @Textediting string +---@param start integer @0-based utf-8 offset of the starting position of the selection +---@param length integer @Size of the utf-8 length of the selection +function ime.ingest(text, start, length) + if #text == 0 then + -- finished textediting + ime.reset() + return "", 0, 0 + end + + ime.editing = true + + if start < 0 then + -- we assume no selection and caret at the end + return text, #text, 0 + end + + -- start is 0-based, so we use start + 1 + local start_byte = utf8.offset(text, start + 1) + if not start_byte then + -- bad start offset + -- we assume it meant the last byte of the text + start_byte = #text + else + start_byte = math.min(start_byte - 1, #text) + end + + if length < 0 then + -- caret only + return text, start_byte, 0 + end + + local end_byte = utf8.offset(text, start + length + 1) + if not end_byte or end_byte - 1 < start_byte then + -- bad length, assume caret only + return text, start_byte, 0 + end + + end_byte = math.min(end_byte - 1, #text) + return text, start_byte, end_byte - start_byte +end + +---Forward the given textediting SDL event data to Views. +---@param text string @Textediting string +---@param start integer @0-based utf-8 offset of the starting position of the selection +---@param length integer @Size of the utf-8 length of the selection +function ime.on_text_editing(text, start, length, ...) + if ime.editing or #text > 0 then + core.root_view:on_ime_text_editing(ime.ingest(text, start, length, ...)) + end +end + +---Stop IME composition. +---Might not completely work on every platform. +function ime.stop() + if ime.editing then + -- SDL_ClearComposition for now doesn't work everywhere + system.clear_ime() + ime.on_text_editing("", 0, 0) + end +end + +---Set the bounding box of the text pertaining the IME. +---The IME will draw its interface based on this info. +---@param x number +---@param y number +---@param w number +---@param h number +function ime.set_location(x, y, w, h) + if not ime.last_location or + ime.last_location.x ~= x or + ime.last_location.y ~= y or + ime.last_location.w ~= w or + ime.last_location.h ~= h + then + ime.last_location.x, ime.last_location.y, ime.last_location.w, ime.last_location.h = x, y, w, h + system.set_text_input_rect(x, y, w, h) + end +end + +ime.reset() +return ime diff --git a/data/core/init.lua b/data/core/init.lua index b5b34c03..36546307 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -6,6 +6,7 @@ local style = require "colors.default" local command local keymap local dirwatch +local ime local RootView local StatusView local TitleView @@ -295,7 +296,19 @@ function core.add_project_directory(path) end return refresh_directory(topdir, dirpath) end, 0.01, 0.01) - coroutine.yield(changed and 0.05 or 0) + -- properly exit coroutine if project not open anymore to clear dir watch + local project_dir_open = false + for _, prj in ipairs(core.project_directories) do + if topdir == prj then + project_dir_open = true + break + end + end + if project_dir_open then + coroutine.yield(changed and 0.05 or 0) + else + return + end end end) @@ -361,6 +374,11 @@ end function core.update_project_subdir(dir, filename, expanded) assert(dir.files_limit, "function should be called only when directory is in files limit mode") dir.shown_subdir[filename] = expanded + if expanded then + dir.watch:watch(dir.name .. PATHSEP .. filename) + else + dir.watch:unwatch(dir.name .. PATHSEP .. filename) + end return refresh_directory(dir, filename) end @@ -477,6 +495,9 @@ local style = require "core.style" -- key binding: -- keymap.add { ["ctrl+escape"] = "core:quit" } +-- pass 'true' for second parameter to overwrite an existing binding +-- keymap.add({ ["ctrl+pageup"] = "root:switch-to-previous-tab" }, true) +-- keymap.add({ ["ctrl+pagedown"] = "root:switch-to-next-tab" }, true) ------------------------------- Fonts ---------------------------------------- @@ -512,11 +533,26 @@ local style = require "core.style" -- enable or disable plugin loading setting config entries: --- enable plugins.trimwhitespace, otherwise it is disable by default: +-- enable plugins.trimwhitespace, otherwise it is disabled by default: -- config.plugins.trimwhitespace = true -- -- disable detectindent, otherwise it is enabled by default -- config.plugins.detectindent = false + +---------------------------- Miscellaneous ------------------------------------- + +-- modify list of files to ignore when indexing the project: +-- config.ignore_files = { +-- -- folders +-- "^%.svn/", "^%.git/", "^%.hg/", "^CVS/", "^%.Trash/", "^%.Trash%-.*/", +-- "^node_modules/", "^%.cache/", "^__pycache__/", +-- -- files +-- "%.pyc$", "%.pyo$", "%.exe$", "%.dll$", "%.obj$", "%.o$", +-- "%.a$", "%.lib$", "%.so$", "%.dylib$", "%.ncb$", "%.sdf$", +-- "%.suo$", "%.pdb$", "%.idb$", "%.class$", "%.psd$", "%.db$", +-- "^desktop%.ini$", "^%.DS_Store$", "^%.directory$", +-- } + ]]) init_file:close() end @@ -558,7 +594,7 @@ local config = require "core.config" -- "^/build.*/" match any top level directory whose name begins with "build" -- "^/subprojects/.+/" match any directory inside a top-level folder named "subprojects". --- You may activate some plugins on a pre-project base to override the user's settings. +-- You may activate some plugins on a per-project basis to override the user's settings. -- config.plugins.trimwitespace = true ]]) init_file:close() @@ -640,6 +676,7 @@ function core.init() command = require "core.command" keymap = require "core.keymap" dirwatch = require "core.dirwatch" + ime = require "core.ime" RootView = require "core.rootview" StatusView = require "core.statusview" TitleView = require "core.titleview" @@ -1034,6 +1071,8 @@ end function core.set_active_view(view) assert(view, "Tried to set active view to nil") + -- Reset the IME even if the focus didn't change + ime.stop() if view ~= core.active_view then if core.active_view and core.active_view.force_focus then core.next_active_view = view @@ -1136,7 +1175,7 @@ function core.custom_log(level, show, backtrace, fmt, ...) text = text, time = os.time(), at = at, - info = backtrace and debug.traceback(nil, 2):gsub("\t", "") + info = backtrace and debug.traceback("", 2):gsub("\t", "") } table.insert(core.log_items, item) if #core.log_items > config.max_log_items then @@ -1185,7 +1224,7 @@ function core.try(fn, ...) local err local ok, res = xpcall(fn, function(msg) local item = core.error("%s", msg) - item.info = debug.traceback(nil, 2):gsub("\t", "") + item.info = debug.traceback("", 2):gsub("\t", "") err = msg end, ...) if ok then @@ -1198,6 +1237,8 @@ function core.on_event(type, ...) local did_keymap = false if type == "textinput" then core.root_view:on_text_input(...) + elseif type == "textediting" then + ime.on_text_editing(...) elseif type == "keypressed" then did_keymap = keymap.on_key_pressed(...) elseif type == "keyreleased" then @@ -1384,7 +1425,7 @@ function core.on_error(err) -- write error to file local fp = io.open(USERDIR .. "/error.txt", "wb") fp:write("Error: " .. tostring(err) .. "\n") - fp:write(debug.traceback(nil, 4) .. "\n") + fp:write(debug.traceback("", 4) .. "\n") fp:close() -- save copy of all unsaved documents for _, doc in ipairs(core.docs) do diff --git a/data/core/keymap-macos.lua b/data/core/keymap-macos.lua index edbd53ef..8fe04b39 100644 --- a/data/core/keymap-macos.lua +++ b/data/core/keymap-macos.lua @@ -6,7 +6,7 @@ local function keymap_macos(keymap) ["cmd+n"] = "core:new-doc", ["cmd+shift+c"] = "core:change-project-folder", ["cmd+shift+o"] = "core:open-project-folder", - ["cmd+shift+r"] = "core:restart", + ["cmd+option+r"] = "core:restart", ["cmd+ctrl+return"] = "core:toggle-fullscreen", ["cmd+ctrl+shift+j"] = "root:split-left", @@ -34,7 +34,9 @@ local function keymap_macos(keymap) ["cmd+8"] = "root:switch-to-tab-8", ["cmd+9"] = "root:switch-to-tab-9", ["wheel"] = "root:scroll", - + ["hwheel"] = "root:horizontal-scroll", + ["shift+hwheel"] = "root:horizontal-scroll", + ["cmd+f"] = "find-replace:find", ["cmd+r"] = "find-replace:replace", ["f3"] = "find-replace:repeat-find", diff --git a/data/core/keymap.lua b/data/core/keymap.lua index 1e082146..28dc9521 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -1,6 +1,7 @@ local core = require "core" local command = require "core.command" local config = require "core.config" +local ime = require "core.ime" local keymap = {} ---@alias keymap.shortcut string @@ -179,6 +180,10 @@ 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 @@ -209,9 +214,32 @@ function keymap.on_key_pressed(k, ...) return false end -function keymap.on_mouse_wheel(delta, ...) - return not (keymap.on_key_pressed("wheel" .. (delta > 0 and "up" or "down"), delta, ...) - or keymap.on_key_pressed("wheel", delta, ...)) +function keymap.on_mouse_wheel(delta_y, delta_x, ...) + local y_direction = delta_y > 0 and "up" or "down" + local x_direction = delta_x > 0 and "left" or "right" + -- Try sending a "cumulative" event for both scroll directions + if delta_y ~= 0 and delta_x ~= 0 then + local result = keymap.on_key_pressed("wheel" .. y_direction .. x_direction, delta_y, delta_x, ...) + if not result then + result = keymap.on_key_pressed("wheelyx", delta_y, delta_x, ...) + end + if result then return true end + end + -- Otherwise send each direction as its own separate event + local y_result, x_result + if delta_y ~= 0 then + y_result = keymap.on_key_pressed("wheel" .. y_direction, delta_y, ...) + if not y_result then + y_result = keymap.on_key_pressed("wheel", delta_y, ...) + end + end + if delta_x ~= 0 then + x_result = keymap.on_key_pressed("wheel" .. x_direction, delta_x, ...) + if not x_result then + x_result = keymap.on_key_pressed("hwheel", delta_x, ...) + end + end + return y_result or x_result end function keymap.on_mouse_pressed(button, x, y, clicks) @@ -274,6 +302,8 @@ keymap.add_direct { ["alt+8"] = "root:switch-to-tab-8", ["alt+9"] = "root:switch-to-tab-9", ["wheel"] = "root:scroll", + ["hwheel"] = "root:horizontal-scroll", + ["shift+wheel"] = "root:horizontal-scroll", ["ctrl+f"] = "find-replace:find", ["ctrl+r"] = "find-replace:replace", diff --git a/data/core/node.lua b/data/core/node.lua index 087610a6..2ce52cc2 100644 --- a/data/core/node.lua +++ b/data/core/node.lua @@ -323,15 +323,14 @@ end function Node:get_scroll_button_rect(index) local w, pad = get_scroll_button_width() local h = style.font:get_height() + style.padding.y * 2 - local x = self.position.x + (index == 1 and 0 or self.size.x - w) + local x = self.position.x + (index == 1 and self.size.x - w * 2 or self.size.x - w) return x, self.position.y, w, h, pad end function Node:get_tab_rect(idx) - local sbw = get_scroll_button_width() - local maxw = self.size.x - 2 * sbw - local x0 = self.position.x + sbw + local maxw = self.size.x + local x0 = self.position.x local x1 = x0 + common.clamp(self.tab_width * (idx - 1) - self.tab_shift, 0, maxw) local x2 = x0 + common.clamp(self.tab_width * idx - self.tab_shift, 0, maxw) local h = style.font:get_height() + style.padding.y * 2 @@ -469,7 +468,10 @@ end function Node:target_tab_width() local n = self:get_visible_tabs_number() - local w = self.size.x - get_scroll_button_width() * 2 + local w = self.size.x + if #self.views > n then + w = self.size.x - get_scroll_button_width() * 2 + end return common.clamp(style.tab_width, w / config.max_tabs, w / n) end @@ -547,24 +549,14 @@ function Node:draw_tab(view, is_active, is_hovered, is_close_hovered, x, y, w, h end function Node:draw_tabs() - local x, y, w, h, scroll_padding = self:get_scroll_button_rect(1) + local _, y, w, h, scroll_padding = self:get_scroll_button_rect(1) + local x = self.position.x local ds = style.divider_size local dots_width = style.font:get_width("…") core.push_clip_rect(x, y, self.size.x, h) renderer.draw_rect(x, y, self.size.x, h, style.background2) renderer.draw_rect(x, y + h - ds, self.size.x, ds, style.divider) - - if self.tab_offset > 1 then - local button_style = self.hovered_scroll_button == 1 and style.text or style.dim - common.draw_text(style.icon_font, button_style, "<", nil, x + scroll_padding, y, 0, h) - end - local tabs_number = self:get_visible_tabs_number() - if #self.views > self.tab_offset + tabs_number - 1 then - local xrb, yrb, wrb = self:get_scroll_button_rect(2) - local button_style = self.hovered_scroll_button == 2 and style.text or style.dim - common.draw_text(style.icon_font, button_style, ">", nil, xrb + scroll_padding, yrb, 0, h) - end for i = self.tab_offset, self.tab_offset + tabs_number - 1 do local view = self.views[i] @@ -574,6 +566,18 @@ function Node:draw_tabs() x, y, w, h) end + if #self.views > tabs_number then + local _, pad = get_scroll_button_width() + local xrb, yrb, wrb, hrb = self:get_scroll_button_rect(1) + renderer.draw_rect(xrb + pad, yrb, wrb * 2, hrb, style.background2) + local left_button_style = (self.hovered_scroll_button == 1 and self.tab_offset > 1) and style.text or style.dim + common.draw_text(style.icon_font, left_button_style, "<", nil, xrb + scroll_padding, yrb, 0, h) + + xrb, yrb, wrb = self:get_scroll_button_rect(2) + local right_button_style = (self.hovered_scroll_button == 2 and #self.views > self.tab_offset + tabs_number - 1) and style.text or style.dim + common.draw_text(style.icon_font, right_button_style, ">", nil, xrb + scroll_padding, yrb, 0, h) + end + core.pop_clip_rect() end diff --git a/data/core/regex.lua b/data/core/regex.lua index fa85d56c..61089283 100644 --- a/data/core/regex.lua +++ b/data/core/regex.lua @@ -2,70 +2,81 @@ -- pattern:gsub(string). regex.__index = function(table, key) return regex[key]; end -regex.match = function(pattern_string, string, offset, options) - local pattern = type(pattern_string) == "table" and - pattern_string or regex.compile(pattern_string) - local res = { regex.cmatch(pattern, string, offset or 1, options or 0) } - res[2] = res[2] and res[2] - 1 +---Looks for the first match of `pattern` in the string `str`. +---If it finds a match, it returns the indices of `str` where this occurrence +---starts and ends; otherwise, it returns `nil`. +---If the pattern has captures, the captured start and end indexes are returned, +---after the two initial ones. +--- +---@param pattern string|table The regex pattern to use, either as a simple string or precompiled. +---@param str string The string to search for valid matches. +---@param offset? integer The position on the subject to start searching. +---@param options? integer A bit field of matching options, eg: regex.NOTBOL | regex.NOTEMPTY +--- +---@return integer? start Offset where the first match was found; `nil` if no match. +---@return integer? end Offset where the first match ends; `nil` if no match. +---@return integer? ... #Captured matches offsets. +regex.find_offsets = function(pattern, str, offset, options) + if type(pattern) ~= "table" then + pattern = regex.compile(pattern) + end + local res = { regex.cmatch(pattern, str, offset or 1, options or 0) } + -- Reduce every end delimiter by 1 + for i = 2,#res,2 do + res[i] = res[i] - 1 + end return table.unpack(res) end --- Will iterate back through any UTF-8 bytes so that we don't replace bits --- mid character. -local function previous_character(str, index) - local byte - repeat - index = index - 1 - byte = string.byte(str, index) - until byte < 128 or byte >= 192 - return index +---Behaves like `string.match`. +---Looks for the first match of `pattern` in the string `str`. +---If it finds a match, it returns the matched string; otherwise, it returns `nil`. +---If the pattern has captures, only the captured strings are returned. +---If a capture is empty, its offset is returned instead. +--- +---@param pattern string|table The regex pattern to use, either as a simple string or precompiled. +---@param str string The string to search for valid matches. +---@param offset? integer The position on the subject to start searching. +---@param options? integer A bit field of matching options, eg: regex.NOTBOL | regex.NOTEMPTY +--- +---@return (string|integer)? ... #List of captured matches; the entire match if no matches were specified; if the match is empty, its offset is returned instead. +regex.match = function(pattern, str, offset, options) + local res = { regex.find(pattern, str, offset, options) } + if #res == 0 then return end + -- If available, only return captures + if #res > 2 then return table.unpack(res, 3) end + return string.sub(str, res[1], res[2]) end --- Moves to the end of the identified character. -local function end_character(str, index) - local byte = string.byte(str, index + 1) - while byte and byte >= 128 and byte < 192 do - index = index + 1 - byte = string.byte(str, index + 1) - end - return index -end - --- Build off matching. For now, only support basic replacements, but capture --- groupings should be doable. We can even have custom group replacements and --- transformations and stuff in lua. Currently, this takes group replacements --- as \1 - \9. --- Should work on UTF-8 text. -regex.gsub = function(pattern_string, str, replacement) - local pattern = type(pattern_string) == "table" and - pattern_string or regex.compile(pattern_string) - local result, indices = "" - local matches, replacements = {}, {} - repeat - indices = { regex.cmatch(pattern, str) } - if #indices > 0 then - table.insert(matches, indices) - local currentReplacement = replacement - if #indices > 2 then - for i = 1, (#indices/2 - 1) do - currentReplacement = string.gsub( - currentReplacement, - "\\" .. i, - str:sub(indices[i*2+1], end_character(str,indices[i*2+2]-1)) - ) - end - end - currentReplacement = string.gsub(currentReplacement, "\\%d", "") - table.insert(replacements, { indices[1], #currentReplacement+indices[1] }) - if indices[1] > 1 then - result = result .. - str:sub(1, previous_character(str, indices[1])) .. currentReplacement - else - result = result .. currentReplacement - end - str = str:sub(indices[2]) +---Behaves like `string.find`. +---Looks for the first match of `pattern` in the string `str`. +---If it finds a match, it returns the indices of `str` where this occurrence +---starts and ends; otherwise, it returns `nil`. +---If the pattern has captures, the captured strings are returned, +---after the two indexes ones. +---If a capture is empty, its offset is returned instead. +--- +---@param pattern string|table The regex pattern to use, either as a simple string or precompiled. +---@param str string The string to search for valid matches. +---@param offset? integer The position on the subject to start searching. +---@param options? integer A bit field of matching options, eg: regex.NOTBOL | regex.NOTEMPTY +--- +---@return integer? start Offset where the first match was found; `nil` if no match. +---@return integer? end Offset where the first match ends; `nil` if no match. +---@return (string|integer)? ... #List of captured matches; if the match is empty, its offset is returned instead. +regex.find = function(pattern, str, offset, options) + local res = { regex.find_offsets(pattern, str, offset, options) } + local out = { } + if #res == 0 then return end + out[1] = res[1] + out[2] = res[2] + for i = 3,#res,2 do + if res[i] > res[i+1] then + -- Like in string.find, if the group has size 0, return the index + table.insert(out, res[i]) + else + table.insert(out, string.sub(str, res[i], res[i+1])) end - until #indices == 0 or indices[1] == indices[2] - return result .. str, matches, replacements + end + return table.unpack(out) end - diff --git a/data/core/rootview.lua b/data/core/rootview.lua index c4eb656f..7230e8e1 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -334,6 +334,9 @@ function RootView:on_text_input(...) core.active_view:on_text_input(...) end +function RootView:on_ime_text_editing(...) + core.active_view:on_ime_text_editing(...) +end function RootView:on_focus_lost(...) -- We force a redraw so documents can redraw without the cursor. diff --git a/data/core/scrollbar.lua b/data/core/scrollbar.lua new file mode 100644 index 00000000..d2bb0562 --- /dev/null +++ b/data/core/scrollbar.lua @@ -0,0 +1,342 @@ +local core = require "core" +local common = require "core.common" +local config = require "core.config" +local style = require "core.style" +local Object = require "core.object" + +---Scrollbar +---Use Scrollbar:set_size to set the bounding box of the view the scrollbar belongs to. +---Use Scrollbar:update to update the scrollbar animations. +---Use Scrollbar:draw to draw the scrollbar. +---Use Scrollbar:on_mouse_pressed, Scrollbar:on_mouse_released, +---Scrollbar:on_mouse_moved and Scrollbar:on_mouse_left to react to mouse movements; +---the scrollbar won't update automatically. +---Use Scrollbar:set_percent to set the scrollbar location externally. +--- +---To manage all the orientations, the scrollbar changes the coordinates system +---accordingly. The "normal" coordinate system adapts the scrollbar coordinates +---as if it's always a vertical scrollbar, positioned at the end of the bounding box. +---@class core.scrollbar : core.object +local Scrollbar = Object:extend() + +---@class ScrollbarOptions +---@field direction "v" | "h" @Vertical or Horizontal +---@field alignment "s" | "e" @Start or End (left to right, top to bottom) +---@field force_status "expanded" | "contracted" | false @Force the scrollbar status +---@field expanded_size number? @Override the default value specified by `style.expanded_scrollbar_size` +---@field contracted_size number? @Override the default value specified by `style.scrollbar_size` + +---@param options ScrollbarOptions +function Scrollbar:new(options) + ---Position information of the owner + self.rect = { + x = 0, y = 0, w = 0, h = 0, + ---Scrollable size + scrollable = 0 + } + self.normal_rect = { + across = 0, + along = 0, + across_size = 0, + along_size = 0, + scrollable = 0 + } + ---@type integer @Position in percent [0-1] + self.percent = 0 + ---@type boolean @Scrollbar dragging status + self.dragging = false + ---@type integer @Private. Used to offset the start of the drag from the top of the thumb + self.drag_start_offset = 0 + ---What is currently being hovered. `thumb` implies` track` + self.hovering = { track = false, thumb = false } + ---@type "v" | "h"@Vertical or Horizontal + self.direction = options.direction or "v" + ---@type "s" | "e" @Start or End (left to right, top to bottom) + self.alignment = options.alignment or "e" + ---@type number @Private. Used to keep track of animations + self.expand_percent = 0 + ---@type "expanded" | "contracted" | false @Force the scrollbar status + self.force_status = options.force_status + self:set_forced_status(options.force_status) + ---@type number? @Override the default value specified by `style.expanded_scrollbar_size` + self.contracted_size = options.contracted_size + ---@type number? @Override the default value specified by `style.scrollbar_size` + self.expanded_size = options.expanded_size +end + + +---Set the status the scrollbar is forced to keep +---@param status "expanded" | "contracted" | false @The status to force +function Scrollbar:set_forced_status(status) + self.force_status = status + if self.force_status == "expanded" then + self.expand_percent = 1 + end +end + + +function Scrollbar:real_to_normal(x, y, w, h) + x, y, w, h = x or 0, y or 0, w or 0, h or 0 + if self.direction == "v" then + if self.alignment == "s" then + x = (self.rect.x + self.rect.w) - x - w + end + return x, y, w, h + else + if self.alignment == "s" then + y = (self.rect.y + self.rect.h) - y - h + end + return y, x, h, w + end +end + + +function Scrollbar:normal_to_real(x, y, w, h) + x, y, w, h = x or 0, y or 0, w or 0, h or 0 + if self.direction == "v" then + if self.alignment == "s" then + x = (self.rect.x + self.rect.w) - x - w + end + return x, y, w, h + else + if self.alignment == "s" then + x = (self.rect.y + self.rect.h) - x - w + end + return y, x, h, w + end +end + + +function Scrollbar:_get_thumb_rect_normal() + local nr = self.normal_rect + local sz = nr.scrollable + if sz == math.huge or sz <= nr.along_size + then + return 0, 0, 0, 0 + end + local scrollbar_size = self.contracted_size or style.scrollbar_size + local expanded_scrollbar_size = self.expanded_size or style.expanded_scrollbar_size + local along_size = math.max(20, nr.along_size * nr.along_size / sz) + local across_size = scrollbar_size + across_size = across_size + (expanded_scrollbar_size - scrollbar_size) * self.expand_percent + return + nr.across + nr.across_size - across_size, + nr.along + self.percent * nr.scrollable * (nr.along_size - along_size) / (sz - nr.along_size), + across_size, + along_size +end + +---Get the thumb rect (the part of the scrollbar that can be dragged) +---@return integer,integer,integer,integer @x, y, w, h +function Scrollbar:get_thumb_rect() + return self:normal_to_real(self:_get_thumb_rect_normal()) +end + + +function Scrollbar:_get_track_rect_normal() + local nr = self.normal_rect + local sz = nr.scrollable + if sz <= nr.along_size or sz == math.huge then + return 0, 0, 0, 0 + end + local scrollbar_size = self.contracted_size or style.scrollbar_size + local expanded_scrollbar_size = self.expanded_size or style.expanded_scrollbar_size + local across_size = scrollbar_size + across_size = across_size + (expanded_scrollbar_size - scrollbar_size) * self.expand_percent + return + nr.across + nr.across_size - across_size, + nr.along, + across_size, + nr.along_size +end + +---Get the track rect (the "background" of the scrollbar) +---@return number,number,number,number @x, y, w, h +function Scrollbar:get_track_rect() + return self:normal_to_real(self:_get_track_rect_normal()) +end + + +function Scrollbar:_overlaps_normal(x, y) + local sx, sy, sw, sh = self:_get_thumb_rect_normal() + local scrollbar_size = self.contracted_size or style.scrollbar_size + local result + if x >= sx - scrollbar_size * 3 and x <= sx + sw and y >= sy and y <= sy + sh then + result = "thumb" + else + sx, sy, sw, sh = self:_get_track_rect_normal() + if x >= sx - scrollbar_size * 3 and x <= sx + sw and y >= sy and y <= sy + sh then + result = "track" + end + end + return result +end + +---Get what part of the scrollbar the coordinates overlap +---@return "thumb"|"track"|nil +function Scrollbar:overlaps(x, y) + x, y = self:real_to_normal(x, y) + return self:_overlaps_normal(x, y) +end + + +function Scrollbar:_on_mouse_pressed_normal(button, x, y, clicks) + local overlaps = self:_overlaps_normal(x, y) + if overlaps then + local _, along, _, along_size = self:_get_thumb_rect_normal() + self.dragging = true + if overlaps == "thumb" then + self.drag_start_offset = along - y + return true + elseif overlaps == "track" then + self.drag_start_offset = - along_size / 2 + return (y - self.normal_rect.along - along_size / 2) / self.normal_rect.along_size + end + end +end + +---Updates the scrollbar with mouse pressed info. +---Won't update the scrollbar position automatically. +---Use Scrollbar:set_percent to update it. +--- +---This sets the dragging status if needed. +--- +---Returns a falsy value if the event happened outside the scrollbar. +---Returns `true` if the thumb was pressed. +---If the track was pressed this returns a value between 0 and 1 +---representing the percent of the position. +---@return boolean|number +function Scrollbar:on_mouse_pressed(button, x, y, clicks) + if button ~= "left" then return end + x, y = self:real_to_normal(x, y) + return self:_on_mouse_pressed_normal(button, x, y, clicks) +end + +---Updates the scrollbar hover status. +---This gets called by other functions and shouldn't be called manually +function Scrollbar:_update_hover_status_normal(x, y) + local overlaps = self:_overlaps_normal(x, y) + self.hovering.thumb = overlaps == "thumb" + self.hovering.track = self.hovering.thumb or overlaps == "track" + return self.hovering.track or self.hovering.thumb +end + +function Scrollbar:_on_mouse_released_normal(button, x, y) + self.dragging = false + return self:_update_hover_status_normal(x, y) +end + +---Updates the scrollbar dragging status +function Scrollbar:on_mouse_released(button, x, y) + if button ~= "left" then return end + x, y = self:real_to_normal(x, y) + return self:_on_mouse_released_normal(button, x, y) +end + + +function Scrollbar:_on_mouse_moved_normal(x, y, dx, dy) + if self.dragging then + local nr = self.normal_rect + return common.clamp((y - nr.along + self.drag_start_offset) / nr.along_size, 0, 1) + end + return self:_update_hover_status_normal(x, y) +end + +---Updates the scrollbar with mouse moved info. +---Won't update the scrollbar position automatically. +---Use Scrollbar:set_percent to update it. +--- +---This updates the hovering status. +--- +---Returns a falsy value if the event happened outside the scrollbar. +---Returns `true` if the scrollbar is hovered. +---If the scrollbar was being dragged, this returns a value between 0 and 1 +---representing the percent of the position. +---@return boolean|number +function Scrollbar:on_mouse_moved(x, y, dx, dy) + x, y = self:real_to_normal(x, y) + dx, dy = self:real_to_normal(dx, dy) -- TODO: do we need this? (is this even correct?) + return self:_on_mouse_moved_normal(x, y, dx, dy) +end + +---Updates the scrollbar hovering status +function Scrollbar:on_mouse_left() + self.hovering.track, self.hovering.thumb = false, false +end + +---Updates the bounding box of the view the scrollbar belongs to. +---@param x number +---@param y number +---@param w number +---@param h number +---@param scrollable number @size of the scrollable area +function Scrollbar:set_size(x, y, w, h, scrollable) + self.rect.x, self.rect.y, self.rect.w, self.rect.h = x, y, w, h + self.rect.scrollable = scrollable + + local nr = self.normal_rect + nr.across, nr.along, nr.across_size, nr.along_size = self:real_to_normal(x, y, w, h) + nr.scrollable = scrollable +end + +---Updates the scrollbar location +---@param percent number @number between 0 and 1 representing the position of the middle part of the thumb +function Scrollbar:set_percent(percent) + self.percent = percent +end + +---Updates the scrollbar animations +function Scrollbar:update() + -- TODO: move the animation code to its own class + if not self.force_status then + local dest = (self.hovering.track or self.dragging) and 1 or 0 + local diff = math.abs(self.expand_percent - dest) + if not config.transitions or diff < 0.05 or config.disabled_transitions["scroll"] then + self.expand_percent = dest + else + local rate = 0.3 + if config.fps ~= 60 or config.animation_rate ~= 1 then + local dt = 60 / config.fps + rate = 1 - common.clamp(1 - rate, 1e-8, 1 - 1e-8)^(config.animation_rate * dt) + end + self.expand_percent = common.lerp(self.expand_percent, dest, rate) + end + if diff > 1e-8 then + core.redraw = true + end + elseif self.force_status == "expanded" then + self.expand_percent = 1 + elseif self.force_status == "contracted" then + self.expand_percent = 0 + end +end + + +---Draw the scrollbar track +function Scrollbar:draw_track() + if not (self.hovering.track or self.dragging) + and self.expand_percent == 0 then + return + end + local color = { table.unpack(style.scrollbar_track) } + color[4] = color[4] * self.expand_percent + local x, y, w, h = self:get_track_rect() + renderer.draw_rect(x, y, w, h, color) +end + +---Draw the scrollbar thumb +function Scrollbar:draw_thumb() + local highlight = self.hovering.thumb or self.dragging + local color = highlight and style.scrollbar2 or style.scrollbar + local x, y, w, h = self:get_thumb_rect() + renderer.draw_rect(x, y, w, h, color) +end + +---Draw both the scrollbar track and thumb +function Scrollbar:draw() + self:draw_track() + self:draw_thumb() +end + + +return Scrollbar diff --git a/data/core/start.lua b/data/core/start.lua index a8f65712..487048a2 100644 --- a/data/core/start.lua +++ b/data/core/start.lua @@ -10,11 +10,12 @@ if MACOS_RESOURCES then DATADIR = MACOS_RESOURCES else local prefix = EXEDIR:match("^(.+)[/\\]bin$") - DATADIR = prefix and (prefix .. '/share/lite-xl') or (EXEDIR .. '/data') + DATADIR = prefix and (prefix .. PATHSEP .. 'share' .. PATHSEP .. 'lite-xl') or (EXEDIR .. PATHSEP .. 'data') end -USERDIR = (system.get_file_info(EXEDIR .. '/user') and (EXEDIR .. '/user')) - or ((os.getenv("XDG_CONFIG_HOME") and os.getenv("XDG_CONFIG_HOME") .. "/lite-xl")) - or (HOME and (HOME .. '/.config/lite-xl')) +USERDIR = (system.get_file_info(EXEDIR .. PATHSEP .. 'user') and (EXEDIR .. PATHSEP .. 'user')) + or os.getenv("LITE_USERDIR") + or ((os.getenv("XDG_CONFIG_HOME") and os.getenv("XDG_CONFIG_HOME") .. PATHSEP .. "lite-xl")) + or (HOME and (HOME .. PATHSEP .. '.config' .. PATHSEP .. 'lite-xl')) package.path = DATADIR .. '/?.lua;' package.path = DATADIR .. '/?/init.lua;' .. package.path diff --git a/data/core/statusview.lua b/data/core/statusview.lua index 48ce24cf..9c4e33fa 100644 --- a/data/core/statusview.lua +++ b/data/core/statusview.lua @@ -11,26 +11,27 @@ local Object = require "core.object" ---@alias core.statusview.styledtext table +---@alias core.statusview.position '"left"' | '"right"' ---A status bar implementation for lite, check core.status_view. ---@class core.statusview : core.view ----@field public super core.view ----@field private items core.statusview.item[] ----@field private active_items core.statusview.item[] ----@field private hovered_item core.statusview.item ----@field private message_timeout number ----@field private message core.statusview.styledtext ----@field private tooltip_mode boolean ----@field private tooltip core.statusview.styledtext ----@field private left_width number ----@field private right_width number ----@field private r_left_width number ----@field private r_right_width number ----@field private left_xoffset number ----@field private right_xoffset number ----@field private dragged_panel '"left"' | '"right"' ----@field private hovered_panel '"left"' | '"right"' ----@field private hide_messages boolean +---@field super core.view +---@field items core.statusview.item[] +---@field active_items core.statusview.item[] +---@field hovered_item core.statusview.item +---@field message_timeout number +---@field message core.statusview.styledtext +---@field tooltip_mode boolean +---@field tooltip core.statusview.styledtext +---@field left_width number +---@field right_width number +---@field r_left_width number +---@field r_right_width number +---@field left_xoffset number +---@field right_xoffset number +---@field dragged_panel '""' | core.statusview.position +---@field hovered_panel '""' | core.statusview.position +---@field hide_messages boolean local StatusView = View:extend() ---Space separator @@ -42,81 +43,73 @@ StatusView.separator = " " StatusView.separator2 = " | " ---@alias core.statusview.item.separator ----|>'core.statusview.separator' # Space separator ----| 'core.statusview.separator2' # Pipe separator +---|>`StatusView.separator` +---| `StatusView.separator2` ---@alias core.statusview.item.predicate fun():boolean ---@alias core.statusview.item.onclick fun(button: string, x: number, y: number) ----@alias core.statusview.item.get_item fun():core.statusview.styledtext,core.statusview.styledtext +---@alias core.statusview.item.get_item fun(self: core.statusview.item):core.statusview.styledtext?,core.statusview.styledtext? ---@alias core.statusview.item.ondraw fun(x, y, h, hovered: boolean, calc_only?: boolean):number ---@class core.statusview.item : core.object ---@field name string ---@field predicate core.statusview.item.predicate ---@field alignment core.statusview.item.alignment ----@field tooltip string | nil +---@field tooltip string ---@field command string | nil @Command to perform when the item is clicked. ----@field on_click core.statusview.item.onclick | nil @Function called when item is clicked and no command is set. ----@field on_draw core.statusview.item.ondraw | nil @Custom drawing that when passed calc true should return the needed width for drawing and when false should draw. +---Function called when item is clicked and no command is set. +---@field on_click core.statusview.item.onclick | nil +---Custom drawing that when passed calc true should return the needed width for +---drawing and when false should draw. +---@field on_draw core.statusview.item.ondraw | nil ---@field background_color renderer.color | nil ---@field background_color_hover renderer.color | nil ---@field visible boolean ---@field separator core.statusview.item.separator ----@field private active boolean ----@field private x number ----@field private w number ----@field private cached_item core.statusview.styledtext +---@field active boolean +---@field x number +---@field w number +---@field cached_item core.statusview.styledtext local StatusViewItem = Object:extend() - ---Available StatusViewItem options. ---@class core.statusview.item.options : table +---A condition to evaluate if the item should be displayed. If a string +---is given it is treated as a require import that should return a valid object +---which is checked against the current active view, the sames applies if a +---table is given. A function that returns a boolean can be used instead to +---perform a custom evaluation, setting to nil means always evaluates to true. ---@field predicate string | table | core.statusview.item.predicate ----@field name string +---A unique name to identify the item on the status bar. +---@field name string @A unique name to identify the item on the status bar. ---@field alignment core.statusview.item.alignment +---A function that should return a core.statusview.styledtext element, +---returning an empty table is allowed. ---@field get_item core.statusview.item.get_item ----@field command? string | core.statusview.item.onclick +---The name of a valid registered command or a callback function to execute +---when the item is clicked. +---@field command string | core.statusview.item.onclick | nil +---The position in which to insert the given item on the internal table, +---a value of -1 inserts the item at the end which is the default. A value +---of 1 will insert the item at the beggining. ---@field position? integer ----@field tooltip? string +---@field tooltip? string @Text displayed when mouse hovers the item. +---@field visible boolean @Flag to show or hide the item +---The type of separator rendered to the right of the item if another item +---follows it. ---@field separator? core.statusview.item.separator -local StatusViewItemOptions = { - ---A condition to evaluate if the item should be displayed. If a string - ---is given it is treated as a require import that should return a valid object - ---which is checked against the current active view, the sames applies if a - ---table is given. A function that returns a boolean can be used instead to - ---perform a custom evaluation, setting to nil means always evaluates to true. - predicate = nil, - ---A unique name to identify the item on the status bar. - name = nil, - alignment = nil, - ---A function that should return a core.statusview.styledtext element, - ---returning empty table is allowed. - get_item = nil, - ---The name of a valid registered command or a callback function to execute - ---when the item is clicked. - command = nil, - ---The position in which to insert the given item on the internal table, - ---a value of -1 inserts the item at the end which is the default. A value - ---of 1 will insert the item at the beggining. - position = nil, - ---Displayed when mouse hovers the item - tooltip = nil, - separator = nil, -} - -StatusViewItem.options = StatusViewItemOptions ---Flag to tell the item should me aligned on left side of status bar. ----@type number +---@type integer StatusViewItem.LEFT = 1 ---Flag to tell the item should me aligned on right side of status bar. ----@type number +---@type integer StatusViewItem.RIGHT = 2 ---@alias core.statusview.item.alignment ----|>'core.statusview.item.LEFT' ----| 'core.statusview.item.RIGHT' +---|>`StatusView.Item.LEFT` +---| `StatusView.Item.RIGHT` ---Constructor ---@param options core.statusview.item.options @@ -210,7 +203,7 @@ function StatusView:register_docview_items() return { dv.doc:is_dirty() and style.accent or style.text, style.icon_font, "f", style.dim, style.font, self.separator2, style.text, - dv.doc.filename and style.text or style.dim, dv.doc:get_name() + dv.doc.filename and style.text or style.dim, common.home_encode(dv.doc:get_name()) } end }) @@ -470,7 +463,7 @@ end ---Hides the given items from the status view or all if no names given. ----@param names table | string | nil +---@param names? table | string function StatusView:hide_items(names) if type(names) == "string" then names = {names} @@ -489,7 +482,7 @@ end ---Shows the given items from the status view or all if no names given. ----@param names table | string | nil +---@param names? table | string function StatusView:show_items(names) if type(names) == "string" then names = {names} @@ -607,8 +600,8 @@ function StatusView:draw_item_tooltip(item) local x = self.pointer.x - (w / 2) - (style.padding.x * 2) if x < 0 then x = 0 end - if x + w + (style.padding.x * 2) > self.size.x then - x = self.size.x - w - (style.padding.x * 2) + if (x + w + (style.padding.x * 3)) > self.size.x then + x = self.size.x - w - (style.padding.x * 3) end renderer.draw_rect( @@ -783,7 +776,7 @@ function StatusView:update_active_items() item.cached_item = {} if item.visible and item:predicate() then local styled_text = type(item.get_item) == "function" - and item.get_item(self) or item.get_item + and item.get_item(item) or item.get_item if #styled_text > 0 then remove_spacing(self, styled_text) @@ -881,7 +874,7 @@ end ---Drag the given panel if possible. ----@param panel '"left"' | '"right"' +---@param panel core.statusview.position ---@param dx number function StatusView:drag_panel(panel, dx) if panel == "left" and self.r_left_width > self.left_width then @@ -915,10 +908,8 @@ end function StatusView:get_hovered_panel(x, y) if y >= self.position.y and x <= self.left_width + style.padding.x then return "left" - else - return "right" end - return "" + return "right" end @@ -1053,9 +1044,13 @@ function StatusView:on_mouse_released(button, x, y) end -function StatusView:on_mouse_wheel(y) - if not self.visible then return end - self:drag_panel(self.hovered_panel, y * self.left_width / 10) +function StatusView:on_mouse_wheel(y, x) + if not self.visible or self.hovered_panel == "" then return end + if x ~= 0 then + self:drag_panel(self.hovered_panel, x * self.left_width / 10) + else + self:drag_panel(self.hovered_panel, y * self.left_width / 10) + end end diff --git a/data/core/syntax.lua b/data/core/syntax.lua index 89208bce..6fe8984a 100644 --- a/data/core/syntax.lua +++ b/data/core/syntax.lua @@ -30,16 +30,21 @@ end local function find(string, field) + local best_match = 0 + local best_syntax for i = #syntax.items, 1, -1 do local t = syntax.items[i] - if common.match_pattern(string, t[field] or {}) then - return t + local s, e = common.match_pattern(string, t[field] or {}) + if s and e - s > best_match then + best_match = e - s + best_syntax = t end end + return best_syntax end function syntax.get(filename, header) - return find(filename, "files") + return find(common.basename(filename), "files") or (header and find(header, "headers")) or plain_text_syntax end diff --git a/data/core/titleview.lua b/data/core/titleview.lua index f9d7a961..69315be1 100644 --- a/data/core/titleview.lua +++ b/data/core/titleview.lua @@ -3,6 +3,14 @@ local common = require "core.common" local style = require "core.style" local View = require "core.view" +local icon_colors = { + bg = { common.color "#2e2e32ff" }, + color6 = { common.color "#e1e1e6ff" }, + color7 = { common.color "#ffa94dff" }, + color8 = { common.color "#93ddfaff" }, + color9 = { common.color "#f7c95cff" } +}; + local restore_command = { symbol = "w", action = function() system.set_window_mode("normal") end } @@ -43,6 +51,10 @@ function TitleView:configure_hit_test(borderless) end end +function TitleView:on_scale_change() + self:configure_hit_test(self.visible) +end + function TitleView:update() self.size.y = self.visible and title_view_height() or 0 title_commands[2] = core.window_mode == "maximized" and restore_command or maximize_command @@ -55,7 +67,11 @@ function TitleView:draw_window_title() local ox, oy = self:get_content_offset() local color = style.text local x, y = ox + style.padding.x, oy + style.padding.y - x = common.draw_text(style.icon_font, color, "M ", nil, x, y, 0, h) + common.draw_text(style.icon_font, icon_colors.bg, "5", nil, x, y, 0, h) + common.draw_text(style.icon_font, icon_colors.color6, "6", nil, x, y, 0, h) + common.draw_text(style.icon_font, icon_colors.color7, "7", nil, x, y, 0, h) + common.draw_text(style.icon_font, icon_colors.color8, "8", nil, x, y, 0, h) + x = common.draw_text(style.icon_font, icon_colors.color9, "9 ", nil, x, y, 0, h) local title = core.compose_window_title(core.window_title) common.draw_text(style.font, color, title, nil, x, y, 0, h) end diff --git a/data/core/tokenizer.lua b/data/core/tokenizer.lua index 0b9b4ac6..5f6d5628 100644 --- a/data/core/tokenizer.lua +++ b/data/core/tokenizer.lua @@ -1,6 +1,5 @@ local core = require "core" local syntax = require "core.syntax" -local common = require "core.common" local tokenizer = {} local bad_patterns = {} @@ -51,31 +50,37 @@ local function push_tokens(t, syn, pattern, full_text, find_results) end end +-- State is a string of bytes, where the count of bytes represents the depth +-- of the subsyntax we are currently in. Each individual byte represents the +-- index of the pattern for the current subsyntax in relation to its parent +-- syntax. Using a string of bytes allows us to have as many subsyntaxes as +-- bytes can be stored on a string while keeping some level of performance in +-- comparison to a Lua table. The only limitation is that a syntax would not +-- be able to contain more than 255 patterns. +-- +-- Lets say a state contains 2 bytes byte #1 with value `3` and byte #2 with +-- a value of `5`. This would mean that on the parent syntax at index `3` a +-- pattern subsyntax that matched current text was found, then inside that +-- subsyntax another subsyntax pattern at index `5` that matched current text +-- was also found. --- State is a 32-bit number that is four separate bytes, illustrating how many --- differnet delimiters we have open, and which subsyntaxes we have active. --- At most, there are 3 subsyntaxes active at the same time. Beyond that, --- does not support further highlighting. +-- Calling `push_subsyntax` appends the current subsyntax pattern index to the +-- state and increases the stack depth. Calling `pop_subsyntax` clears the +-- last appended subsyntax and decreases the stack. --- You can think of it as a maximum 4 integer (0-255) stack. It always has --- 1 integer in it. Calling `push_subsyntax` increases the stack depth. Calling --- `pop_subsyntax` decreases it. The integers represent the index of a pattern --- that we're following in the syntax. The top of the stack can be any valid --- pattern index, any integer lower in the stack must represent a pattern that --- specifies a subsyntax. - --- If you do not have subsyntaxes in your syntax, the three most --- singificant numbers will always be 0, the stack will only ever be length 1 --- and the state variable will only ever range from 0-255. local function retrieve_syntax_state(incoming_syntax, state) local current_syntax, subsyntax_info, current_pattern_idx, current_level = - incoming_syntax, nil, state, 0 - if state > 0 and (state > 255 or current_syntax.patterns[state].syntax) then - -- If we have higher bits, then decode them one at a time, and find which + incoming_syntax, nil, state:byte(1) or 0, 1 + if + current_pattern_idx > 0 + and + current_syntax.patterns[current_pattern_idx] + then + -- If the state is not empty we iterate over each byte, and find which -- syntax we're using. Rather than walking the bytes, and calling into -- `syntax` each time, we could probably cache this in a single table. - for i = 0, 2 do - local target = bit32.extract(state, i*8, 8) + for i = 1, #state do + local target = state:byte(i) if target ~= 0 then if current_syntax.patterns[target].syntax then subsyntax_info = current_syntax.patterns[target] @@ -95,6 +100,21 @@ local function retrieve_syntax_state(incoming_syntax, state) return current_syntax, subsyntax_info, current_pattern_idx, current_level end +---Return the list of syntaxes used in the specified state. +---@param base_syntax table @The initial base syntax (the syntax of the file) +---@param state string @The state of the tokenizer to extract from +---@return table @Array of syntaxes starting from the innermost one +function tokenizer.extract_subsyntaxes(base_syntax, state) + local current_syntax + local t = {} + repeat + current_syntax = retrieve_syntax_state(base_syntax, state) + table.insert(t, current_syntax) + state = string.sub(state, 2) + until #state == 0 + return t +end + local function report_bad_pattern(log_fn, syntax, pattern_idx, msg, ...) if not bad_patterns[syntax] then bad_patterns[syntax] = { } @@ -107,7 +127,7 @@ end ---@param incoming_syntax table ---@param text string ----@param state integer +---@param state string function tokenizer.tokenize(incoming_syntax, text, state) local res = {} local i = 1 @@ -116,9 +136,9 @@ function tokenizer.tokenize(incoming_syntax, text, state) return { "normal", text } end - state = state or 0 + state = state or string.char(0) -- incoming_syntax : the parent syntax of the file. - -- state : a 32-bit number representing syntax state (see above) + -- state : a string of bytes representing syntax state (see above) -- current_syntax : the syntax we're currently in. -- subsyntax_info : info about the delimiters of this subsyntax. @@ -130,7 +150,18 @@ function tokenizer.tokenize(incoming_syntax, text, state) -- Should be used to set the state variable. Don't modify it directly. local function set_subsyntax_pattern_idx(pattern_idx) current_pattern_idx = pattern_idx - state = bit32.replace(state, pattern_idx, current_level*8, 8) + local state_len = #state + if current_level > state_len then + state = state .. string.char(pattern_idx) + elseif state_len == 1 then + state = string.char(pattern_idx) + else + state = ("%s%s%s"):format( + state:sub(1,current_level-1), + string.char(pattern_idx), + state:sub(current_level+1) + ) + end end @@ -144,8 +175,8 @@ function tokenizer.tokenize(incoming_syntax, text, state) end local function pop_subsyntax() - set_subsyntax_pattern_idx(0) current_level = current_level - 1 + state = string.sub(state, 1, current_level) set_subsyntax_pattern_idx(0) current_syntax, subsyntax_info, current_pattern_idx, current_level = retrieve_syntax_state(incoming_syntax, state) @@ -183,23 +214,12 @@ function tokenizer.tokenize(incoming_syntax, text, state) return end res = p.pattern and { text:ufind((at_start or p.whole_line[p_idx]) and "^" .. code or code, next) } - or { regex.match(code, text, text:ucharpos(next), (at_start or p.whole_line[p_idx]) and regex.ANCHORED or 0) } + or { regex.find(code, text, text:ucharpos(next), (at_start or p.whole_line[p_idx]) and regex.ANCHORED or 0) } if p.regex and #res > 0 then -- set correct utf8 len for regex result - local char_pos_1 = string.ulen(text:sub(1, res[1])) - local char_pos_2 = char_pos_1 + string.ulen(text:sub(res[1], res[2])) - 1 - -- `regex.match` returns group results as a series of `begin, end` - -- we only want `begin`s - if #res >= 3 then - res[3] = char_pos_1 + string.ulen(text:sub(res[1], res[3])) - 1 - end - for i=1,(#res-3) do - local curr = i + 3 - local from = i * 2 + 3 - if from < #res then - res[curr] = char_pos_1 + string.ulen(text:sub(res[1], res[from])) - 1 - else - res[curr] = nil - end + local char_pos_1 = res[1] > next and string.ulen(text:sub(1, res[1])) or next + local char_pos_2 = string.ulen(text:sub(1, res[2])) + for i=3,#res do + res[i] = string.ulen(text:sub(1, res[i] - 1)) + 1 end res[1] = char_pos_1 res[2] = char_pos_2 @@ -317,7 +337,7 @@ function tokenizer.tokenize(incoming_syntax, text, state) end end end - + return res, state end diff --git a/data/core/view.lua b/data/core/view.lua index 04d01230..77378b90 100644 --- a/data/core/view.lua +++ b/data/core/view.lua @@ -1,8 +1,8 @@ local core = require "core" local config = require "core.config" -local style = require "core.style" local common = require "core.common" local Object = require "core.object" +local Scrollbar = require "core.scrollbar" ---@class core.view.position ---@field x number @@ -28,10 +28,6 @@ local Object = require "core.object" ---@field w core.view.thumbtrackwidth ---@field h core.view.thumbtrack ----@class core.view.increment ----@field value number ----@field to number - ---@alias core.view.cursor "'arrow'" | "'ibeam'" | "'sizeh'" | "'sizev'" | "'hand'" ---@alias core.view.mousebutton "'left'" | "'right'" @@ -47,8 +43,9 @@ local Object = require "core.object" ---@field scroll core.view.scroll ---@field cursor core.view.cursor ---@field scrollable boolean ----@field scrollbar core.view.scrollbar ----@field scrollbar_alpha core.view.increment +---@field v_scrollbar core.scrollbar +---@field h_scrollbar core.scrollbar +---@field current_scale number local View = Object:extend() -- context can be "application" or "session". The instance of objects @@ -62,13 +59,9 @@ function View:new() self.scroll = { x = 0, y = 0, to = { x = 0, y = 0 } } self.cursor = "arrow" self.scrollable = false - self.scrollbar = { - x = { thumb = 0, track = 0 }, - y = { thumb = 0, track = 0 }, - w = { thumb = 0, track = 0, to = { thumb = 0, track = 0 } }, - h = { thumb = 0, track = 0 }, - } - self.scrollbar_alpha = { value = 0, to = 0 } + self.v_scrollbar = Scrollbar({direction = "v", alignment = "e"}) + self.h_scrollbar = Scrollbar({direction = "h", alignment = "e"}) + self.current_scale = SCALE end function View:move_towards(t, k, dest, rate, name) @@ -109,47 +102,9 @@ function View:get_scrollable_size() return math.huge end - ----@return number x ----@return number y ----@return number width ----@return number height -function View:get_scrollbar_track_rect() - local sz = self:get_scrollable_size() - if sz <= self.size.y or sz == math.huge then - return 0, 0, 0, 0 - end - local width = style.scrollbar_size - if self.hovered_scrollbar_track or self.dragging_scrollbar then - width = style.expanded_scrollbar_size - end - return - self.position.x + self.size.x - width, - self.position.y, - width, - self.size.y -end - - ----@return number x ----@return number y ----@return number width ----@return number height -function View:get_scrollbar_rect() - local sz = self:get_scrollable_size() - if sz <= self.size.y or sz == math.huge then - return 0, 0, 0, 0 - end - local h = math.max(20, self.size.y * self.size.y / sz) - local width = style.scrollbar_size - if self.hovered_scrollbar_track or self.dragging_scrollbar then - width = style.expanded_scrollbar_size - end - return - self.position.x + self.size.x - width, - self.position.y + self.scroll.y * (self.size.y - h) / (sz - self.size.y), - width, - h +---@return number +function View:get_h_scrollable_size() + return 0 end @@ -157,16 +112,19 @@ end ---@param y number ---@return boolean function View:scrollbar_overlaps_point(x, y) - local sx, sy, sw, sh = self:get_scrollbar_rect() - return x >= sx - style.scrollbar_size * 3 and x < sx + sw and y > sy and y <= sy + sh + return not (not (self.v_scrollbar:overlaps(x, y) or self.h_scrollbar:overlaps(x, y))) end ----@param x number ----@param y number + ---@return boolean -function View:scrollbar_track_overlaps_point(x, y) - local sx, sy, sw, sh = self:get_scrollbar_track_rect() - return x >= sx - style.scrollbar_size * 3 and x < sx + sw and y > sy and y <= sy + sh +function View:scrollbar_dragging() + return self.v_scrollbar.dragging or self.h_scrollbar.dragging +end + + +---@return boolean +function View:scrollbar_hovering() + return self.v_scrollbar.hovering.track or self.h_scrollbar.hovering.track end @@ -176,14 +134,18 @@ end ---@param clicks integer ---return boolean function View:on_mouse_pressed(button, x, y, clicks) - if self:scrollbar_track_overlaps_point(x, y) then - if self:scrollbar_overlaps_point(x, y) then - self.dragging_scrollbar = true - else - local _, _, _, sh = self:get_scrollbar_rect() - local ly = (y - self.position.y) - sh / 2 - local pct = common.clamp(ly / self.size.y, 0, 100) - self.scroll.to.y = self:get_scrollable_size() * pct + if not self.scrollable then return end + local result = self.v_scrollbar:on_mouse_pressed(button, x, y, clicks) + if result then + if result ~= true then + self.scroll.to.y = result * self:get_scrollable_size() + end + return true + end + result = self.h_scrollbar:on_mouse_pressed(button, x, y, clicks) + if result then + if result ~= true then + self.scroll.to.x = result * self:get_h_scrollable_size() end return true end @@ -194,7 +156,9 @@ end ---@param x number ---@param y number function View:on_mouse_released(button, x, y) - self.dragging_scrollbar = false + if not self.scrollable then return end + self.v_scrollbar:on_mouse_released(button, x, y) + self.h_scrollbar:on_mouse_released(button, x, y) end @@ -203,22 +167,41 @@ end ---@param dx number ---@param dy number function View:on_mouse_moved(x, y, dx, dy) - if self.dragging_scrollbar then - local delta = self:get_scrollable_size() / self.size.y * dy - self.scroll.to.y = self.scroll.to.y + delta - if not config.animate_drag_scroll then - self:clamp_scroll_position() - self.scroll.y = self.scroll.to.y + if not self.scrollable then return end + local result + if self.h_scrollbar.dragging then goto skip_v_scrollbar end + result = self.v_scrollbar:on_mouse_moved(x, y, dx, dy) + if result then + if result ~= true then + self.scroll.to.y = result * self:get_scrollable_size() + if not config.animate_drag_scroll then + self:clamp_scroll_position() + self.scroll.y = self.scroll.to.y + end end + -- hide horizontal scrollbar + self.h_scrollbar:on_mouse_left() + return true + end + ::skip_v_scrollbar:: + result = self.h_scrollbar:on_mouse_moved(x, y, dx, dy) + if result then + if result ~= true then + self.scroll.to.x = result * self:get_h_scrollable_size() + if not config.animate_drag_scroll then + self:clamp_scroll_position() + self.scroll.x = self.scroll.to.x + end + end + return true end - self.hovered_scrollbar = self:scrollbar_overlaps_point(x, y) - self.hovered_scrollbar_track = self.hovered_scrollbar or self:scrollbar_track_overlaps_point(x, y) end function View:on_mouse_left() - self.hovered_scrollbar = false - self.hovered_scrollbar_track = false + if not self.scrollable then return end + self.v_scrollbar:on_mouse_left() + self.h_scrollbar:on_mouse_left() end @@ -236,12 +219,25 @@ function View:on_text_input(text) -- no-op end ----@param y number ----@return boolean -function View:on_mouse_wheel(y) +function View:on_ime_text_editing(text, start, length) + -- no-op end + +---@param y number @Vertical scroll delta; positive is "up" +---@param x number @Horizontal scroll delta; positive is "left" +---@return boolean @Capture event +function View:on_mouse_wheel(y, x) + -- no-op +end + +---Can be overriden to listen for scale change events to apply +---any neccesary changes in sizes, padding, etc... +---@param new_scale number +---@param prev_scale number +function View:on_scale_change(new_scale, prev_scale) end + function View:get_content_bounds() local x = self.scroll.x local y = self.scroll.y @@ -261,35 +257,35 @@ end function View:clamp_scroll_position() local max = self:get_scrollable_size() - self.size.y self.scroll.to.y = common.clamp(self.scroll.to.y, 0, max) + + max = self:get_h_scrollable_size() - self.size.x + self.scroll.to.x = common.clamp(self.scroll.to.x, 0, max) end function View:update_scrollbar() - local x, y, w, h = self:get_scrollbar_rect() - self.scrollbar.w.to.thumb = w - self:move_towards(self.scrollbar.w, "thumb", self.scrollbar.w.to.thumb, 0.3, "scroll") - self.scrollbar.x.thumb = x + w - self.scrollbar.w.thumb - self.scrollbar.y.thumb = y - self.scrollbar.h.thumb = h + local v_scrollable = self:get_scrollable_size() + self.v_scrollbar:set_size(self.position.x, self.position.y, self.size.x, self.size.y, v_scrollable) + self.v_scrollbar:set_percent(self.scroll.y/v_scrollable) + self.v_scrollbar:update() - local x, y, w, h = self:get_scrollbar_track_rect() - self.scrollbar.w.to.track = w - self:move_towards(self.scrollbar.w, "track", self.scrollbar.w.to.track, 0.3, "scroll") - self.scrollbar.x.track = x + w - self.scrollbar.w.track - self.scrollbar.y.track = y - self.scrollbar.h.track = h - - -- we use 100 for a smoother transition - self.scrollbar_alpha.to = (self.hovered_scrollbar_track or self.dragging_scrollbar) and 100 or 0 - self:move_towards(self.scrollbar_alpha, "value", self.scrollbar_alpha.to, 0.3, "scroll") + local h_scrollable = self:get_h_scrollable_size() + self.h_scrollbar:set_size(self.position.x, self.position.y, self.size.x, self.size.y, h_scrollable) + self.h_scrollbar:set_percent(self.scroll.x/h_scrollable) + self.h_scrollbar:update() end function View:update() + if self.current_scale ~= SCALE then + self:on_scale_change(SCALE, self.current_scale) + self.current_scale = SCALE + end + self:clamp_scroll_position() self:move_towards(self.scroll, "x", self.scroll.to.x, 0.3, "scroll") self:move_towards(self.scroll, "y", self.scroll.to.y, 0.3, "scroll") - + if not self.scrollable then return end self:update_scrollbar() end @@ -302,29 +298,9 @@ function View:draw_background(color) end -function View:draw_scrollbar_track() - if not (self.hovered_scrollbar_track or self.dragging_scrollbar) - and self.scrollbar_alpha.value == 0 then - return - end - local color = { table.unpack(style.scrollbar_track) } - color[4] = color[4] * self.scrollbar_alpha.value / 100 - renderer.draw_rect(self.scrollbar.x.track, self.scrollbar.y.track, - self.scrollbar.w.track, self.scrollbar.h.track, color) -end - - -function View:draw_scrollbar_thumb() - local highlight = self.hovered_scrollbar or self.dragging_scrollbar - local color = highlight and style.scrollbar2 or style.scrollbar - renderer.draw_rect(self.scrollbar.x.thumb, self.scrollbar.y.thumb, - self.scrollbar.w.thumb, self.scrollbar.h.thumb, color) -end - - function View:draw_scrollbar() - self:draw_scrollbar_track() - self:draw_scrollbar_thumb() + self.v_scrollbar:draw() + self.h_scrollbar:draw() end diff --git a/data/fonts/icons.ttf b/data/fonts/icons.ttf index 43ab3767..5fd6d9e9 100644 Binary files a/data/fonts/icons.ttf and b/data/fonts/icons.ttf differ diff --git a/data/plugins/autocomplete.lua b/data/plugins/autocomplete.lua index d638d4ff..5921e838 100644 --- a/data/plugins/autocomplete.lua +++ b/data/plugins/autocomplete.lua @@ -588,8 +588,11 @@ function autocomplete.open(on_close) end local av = get_active_view() - last_line, last_col = av.doc:get_selection() - update_suggestions() + if av then + partial = get_partial_symbol() + last_line, last_col = av.doc:get_selection() + update_suggestions() + end end function autocomplete.close() @@ -645,11 +648,11 @@ command.add(predicate, { end, ["autocomplete:previous"] = function() - suggestions_idx = math.max(suggestions_idx - 1, 1) + suggestions_idx = (suggestions_idx - 2) % #suggestions + 1 end, ["autocomplete:next"] = function() - suggestions_idx = math.min(suggestions_idx + 1, #suggestions) + suggestions_idx = (suggestions_idx % #suggestions) + 1 end, ["autocomplete:cycle"] = function() diff --git a/data/plugins/detectindent.lua b/data/plugins/detectindent.lua index 3ab0ee45..f2589ead 100644 --- a/data/plugins/detectindent.lua +++ b/data/plugins/detectindent.lua @@ -75,7 +75,9 @@ local function escape_comment_tokens(token) end -local function get_comment_patterns(syntax) +local function get_comment_patterns(syntax, _loop) + _loop = _loop or 1 + if _loop > 5 then return end if comments_cache[syntax] then if #comments_cache[syntax] > 0 then return comments_cache[syntax] @@ -125,7 +127,7 @@ local function get_comment_patterns(syntax) elseif pattern.syntax then local subsyntax = type(pattern.syntax) == 'table' and pattern.syntax or core_syntax.get("file"..pattern.syntax, "") - local sub_comments = get_comment_patterns(subsyntax) + local sub_comments = get_comment_patterns(subsyntax, _loop + 1) if sub_comments then for s=1, #sub_comments do table.insert(comments, sub_comments[s]) @@ -190,11 +192,11 @@ local function get_non_empty_lines(syntax, lines) end else if comment[3] then - local start, ending = regex.match( + local start, ending = regex.find_offsets( comment[2], line, 1, regex.ANCHORED ) if start then - if not regex.match( + if not regex.find_offsets( comment[3], line, ending+1, regex.ANCHORED ) then @@ -204,7 +206,7 @@ local function get_non_empty_lines(syntax, lines) end break end - elseif regex.match(comment[2], line, 1, regex.ANCHORED) then + elseif regex.find_offsets(comment[2], line, 1, regex.ANCHORED) then is_comment = true break end @@ -214,7 +216,7 @@ local function get_non_empty_lines(syntax, lines) is_comment = true inside_comment = false end_pattern = nil - elseif end_regex and regex.match(end_regex, line) then + elseif end_regex and regex.find_offsets(end_regex, line) then is_comment = true inside_comment = false end_regex = nil diff --git a/data/plugins/drawwhitespace.lua b/data/plugins/drawwhitespace.lua index a0b8ad60..89be875d 100644 --- a/data/plugins/drawwhitespace.lua +++ b/data/plugins/drawwhitespace.lua @@ -244,7 +244,7 @@ function DocView:draw_line_text(idx, x, y) local color = base_color local draw = false - if e == #text - 1 then + if e >= #text - 1 then draw = show_trailing color = trailing_color elseif s == 1 then @@ -290,14 +290,15 @@ function DocView:draw_line_text(idx, x, y) local ty = y + self:get_line_text_y_offset() local cache = ws_cache[self.doc.highlighter][idx] for i=1,#cache,4 do - local sub = cache[i] local tx = cache[i + 1] + x local tw = cache[i + 2] - local color = cache[i + 3] - if tx + tw >= x1 then - tx = renderer.draw_text(font, sub, tx, ty, color) + if tx <= x2 then + local sub = cache[i] + local color = cache[i + 3] + if tx + tw >= x1 then + tx = renderer.draw_text(font, sub, tx, ty, color) + end end - if tx > x2 then break end end return draw_line_text(self, idx, x, y) diff --git a/data/plugins/language_md.lua b/data/plugins/language_md.lua index 72b77681..d0f76838 100644 --- a/data/plugins/language_md.lua +++ b/data/plugins/language_md.lua @@ -42,9 +42,9 @@ syntax.add { -- blockquote { pattern = "^%s*>+%s", type = "string" }, -- alternative bold italic formats - { pattern = { "%s___", "___%f[%s]" }, type = "markdown_bold_italic" }, - { pattern = { "%s__", "__%f[%s]" }, type = "markdown_bold" }, - { pattern = { "%s_[%S]", "_%f[%s]" }, type = "markdown_italic" }, + { pattern = { "%s___", "___" }, type = "markdown_bold_italic" }, + { pattern = { "%s__", "__" }, type = "markdown_bold" }, + { pattern = { "%s_[%S]", "_" }, type = "markdown_italic" }, -- reference links { pattern = "^%s*%[%^()["..in_squares_match.."]+()%]: ", @@ -112,6 +112,7 @@ syntax.add { { pattern = { "%$%$", "%$%$", "\\" }, type = "string", syntax = ".tex"}, { regex = { "\\$", [[\$|(?=\\*\n)]], "\\" }, type = "string", syntax = ".tex"}, -- code blocks + { pattern = { "```caddyfile", "```" }, type = "string", syntax = "Caddyfile" }, { pattern = { "```c++", "```" }, type = "string", syntax = ".cpp" }, { pattern = { "```cpp", "```" }, type = "string", syntax = ".cpp" }, { pattern = { "```python", "```" }, type = "string", syntax = ".py" }, @@ -149,14 +150,15 @@ syntax.add { { pattern = { "```", "```" }, type = "string" }, { pattern = { "``", "``" }, type = "string" }, { pattern = { "%f[\\`]%`[%S]", "`" }, type = "string" }, + -- lines + { pattern = "^%-%-%-+\n" , type = "comment" }, + { pattern = "^%*%*%*+\n", type = "comment" }, + { pattern = "^___+\n", type = "comment" }, + { pattern = "^===+\n", type = "comment" }, -- strike { pattern = { "~~", "~~" }, type = "keyword2" }, -- highlight { pattern = { "==", "==" }, type = "literal" }, - -- lines - { pattern = "^%-%-%-+$" , type = "comment" }, - { pattern = "^%*%*%*+$", type = "comment" }, - { pattern = "^___+$", type = "comment" }, -- bold and italic { pattern = { "%*%*%*%S", "%*%*%*" }, type = "markdown_bold_italic" }, { pattern = { "%*%*%S", "%*%*" }, type = "markdown_bold" }, @@ -166,16 +168,16 @@ syntax.add { type = "markdown_italic" }, -- alternative bold italic formats - { pattern = "^___[%s%p%w]+___%s" , type = "markdown_bold_italic" }, - { pattern = "^__[%s%p%w]+__%s" , type = "markdown_bold" }, - { pattern = "^_[%s%p%w]+_%s" , type = "markdown_italic" }, + { pattern = "^___[%s%p%w]+___" , type = "markdown_bold_italic" }, + { pattern = "^__[%s%p%w]+__" , type = "markdown_bold" }, + { pattern = "^_[%s%p%w]+_" , type = "markdown_italic" }, -- heading with custom id { pattern = "^#+%s[%w%s%p]+(){()#[%w%-]+()}", type = { "keyword", "function", "string", "function" } }, -- headings - { pattern = "^#+%s.+$", type = "keyword" }, + { pattern = "^#+%s.+\n", type = "keyword" }, -- superscript and subscript { pattern = "%^()%d+()%^", diff --git a/data/plugins/lineguide.lua b/data/plugins/lineguide.lua index efe606dd..63f9a9ca 100644 --- a/data/plugins/lineguide.lua +++ b/data/plugins/lineguide.lua @@ -86,7 +86,7 @@ function DocView:draw_overlay(...) and config.plugins.lineguide.enabled and - not self:is(CommandView) + self:is(DocView) then local line_x = self:get_line_screen_position(1) local character_width = self:get_font():get_width("n") diff --git a/data/plugins/linewrapping.lua b/data/plugins/linewrapping.lua index 66b303ee..b287c523 100644 --- a/data/plugins/linewrapping.lua +++ b/data/plugins/linewrapping.lua @@ -219,7 +219,7 @@ function LineWrapping.draw_guide(docview) end function LineWrapping.update_docview_breaks(docview) - local x,y,w,h = docview:get_scrollbar_rect() + local x,y,w,h = docview.v_scrollbar:get_thumb_rect() local width = (type(config.plugins.linewrapping.width_override) == "function" and config.plugins.linewrapping.width_override(docview)) or config.plugins.linewrapping.width_override or (docview.size.x - docview:get_gutter_width() - w) if (not docview.wrapped_settings or docview.wrapped_settings.width == nil or width ~= docview.wrapped_settings.width) then @@ -355,18 +355,34 @@ function DocView:get_scrollable_size() return self:get_line_height() * (get_total_wrapped_lines(self) - 1) + self.size.y end +local old_get_h_scrollable_size = DocView.get_h_scrollable_size +function DocView:get_h_scrollable_size(...) + if self.wrapping_enabled then return 0 end + return old_get_h_scrollable_size(self, ...) +end + local old_new = DocView.new function DocView:new(doc) old_new(self, doc) if not open_files[doc] then open_files[doc] = {} end table.insert(open_files[doc], self) if config.plugins.linewrapping.enable_by_default then + self.wrapping_enabled = true LineWrapping.update_docview_breaks(self) + else + self.wrapping_enabled = false end end +local old_scroll_to_line = DocView.scroll_to_line +function DocView:scroll_to_line(...) + if self.wrapping_enabled then LineWrapping.update_docview_breaks(self) end + old_scroll_to_line(self, ...) +end + local old_scroll_to_make_visible = DocView.scroll_to_make_visible function DocView:scroll_to_make_visible(line, col) + if self.wrapping_enabled then LineWrapping.update_docview_breaks(self) end old_scroll_to_make_visible(self, line, col) if self.wrapped_settings then self.scroll.to.x = 0 end end @@ -557,11 +573,13 @@ end command.add(nil, { ["line-wrapping:enable"] = function() if core.active_view and core.active_view.doc then + core.active_view.wrapping_enabled = true LineWrapping.update_docview_breaks(core.active_view) end end, ["line-wrapping:disable"] = function() if core.active_view and core.active_view.doc then + core.active_view.wrapping_enabled = false LineWrapping.reconstruct_breaks(core.active_view, core.active_view:get_font(), math.huge) end end, diff --git a/data/plugins/projectsearch.lua b/data/plugins/projectsearch.lua index 9a0b6a93..cc5e1324 100644 --- a/data/plugins/projectsearch.lua +++ b/data/plugins/projectsearch.lua @@ -6,7 +6,7 @@ local command = require "core.command" local style = require "core.style" local View = require "core.view" - +---@class plugins.projectsearch.resultsview : core.view local ResultsView = View:extend() ResultsView.context = "session" @@ -219,6 +219,10 @@ function ResultsView:draw() end +---@param path string +---@param text string +---@param fn fun(line_text:string):... +---@return plugins.projectsearch.resultsview? local function begin_search(path, text, fn) if text == "" then core.error("Expected non-empty string") @@ -226,6 +230,7 @@ local function begin_search(path, text, fn) end local rv = ResultsView(path, text, fn) core.root_view:get_active_node_default():add_view(rv) + return rv end @@ -249,6 +254,59 @@ local function normalize_path(path) return path end +---@class plugins.projectsearch +local projectsearch = {} + +---@type plugins.projectsearch.resultsview +projectsearch.ResultsView = ResultsView + +---@param text string +---@param path string +---@param insensitive? boolean +---@return plugins.projectsearch.resultsview? +function projectsearch.search_plain(text, path, insensitive) + if insensitive then text = text:lower() end + return begin_search(path, text, function(line_text) + if insensitive then + return line_text:lower():find(text, nil, true) + else + return line_text:find(text, nil, true) + end + end) +end + +---@param text string +---@param path string +---@param insensitive? boolean +---@return plugins.projectsearch.resultsview? +function projectsearch.search_regex(text, path, insensitive) + local re, errmsg + if insensitive then + re, errmsg = regex.compile(text, "i") + else + re, errmsg = regex.compile(text) + end + if not re then core.log("%s", errmsg) return end + return begin_search(path, text, function(line_text) + return regex.cmatch(re, line_text) + end) +end + +---@param text string +---@param path string +---@param insensitive? boolean +---@return plugins.projectsearch.resultsview? +function projectsearch.search_fuzzy(text, path, insensitive) + if insensitive then text = text:lower() end + return begin_search(path, text, function(line_text) + if insensitive then + return common.fuzzy_match(line_text:lower(), text) and 1 + else + return common.fuzzy_match(line_text, text) and 1 + end + end) +end + command.add(nil, { ["project-search:find"] = function(path) @@ -256,10 +314,7 @@ command.add(nil, { text = get_selected_text(), select_text = true, submit = function(text) - text = text:lower() - begin_search(path, text, function(line_text) - return line_text:lower():find(text, nil, true) - end) + projectsearch.search_plain(text, path, true) end }) end, @@ -267,10 +322,7 @@ command.add(nil, { ["project-search:find-regex"] = function(path) core.command_view:enter("Find Regex In " .. (normalize_path(path) or "Project"), { submit = function(text) - local re = regex.compile(text, "i") - begin_search(path, text, function(line_text) - return regex.cmatch(re, line_text) - end) + projectsearch.search_regex(text, path, true) end }) end, @@ -280,9 +332,7 @@ command.add(nil, { text = get_selected_text(), select_text = true, submit = function(text) - begin_search(path, text, function(line_text) - return common.fuzzy_match(line_text, text) and 1 - end) + projectsearch.search_fuzzy(text, path, true) end }) end, @@ -344,3 +394,6 @@ keymap.add { ["home"] = "project-search:move-to-start-of-doc", ["end"] = "project-search:move-to-end-of-doc" } + + +return projectsearch diff --git a/data/plugins/toolbarview.lua b/data/plugins/toolbarview.lua index f6c3275a..9cfa15e8 100644 --- a/data/plugins/toolbarview.lua +++ b/data/plugins/toolbarview.lua @@ -39,6 +39,11 @@ end function ToolbarView:toggle_visible() self.visible = not self.visible + if self.tooltip then + core.status_view:remove_tooltip() + self.tooltip = false + end + self.hovered_item = nil end function ToolbarView:get_icon_width() @@ -73,6 +78,7 @@ end function ToolbarView:draw() + if not self.visible then return end self:draw_background(style.background2) for item, x, y, w, h in self:each_item() do @@ -83,6 +89,7 @@ end function ToolbarView:on_mouse_pressed(button, x, y, clicks) + if not self.visible then return end local caught = ToolbarView.super.on_mouse_pressed(self, button, x, y, clicks) if caught then return caught end core.set_active_view(core.last_active_view) @@ -94,6 +101,7 @@ end function ToolbarView:on_mouse_moved(px, py, ...) + if not self.visible then return end ToolbarView.super.on_mouse_moved(self, px, py, ...) self.hovered_item = nil local x_min, x_max, y_min, y_max = self.size.x, 0, self.size.y, 0 diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index c2281fac..bf42d944 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -50,20 +50,6 @@ function TreeView:new() self.item_icon_width = 0 self.item_text_spacing = 0 - - self:add_core_hooks() -end - - -function TreeView:add_core_hooks() - -- When a file or directory is deleted we delete the corresponding cache entry - -- because if the entry is recreated we may use wrong information from cache. - local on_delete = core.on_dirmonitor_delete - core.on_dirmonitor_delete = function(dir, filepath) - local cache = self.cache[dir.name] - if cache then cache[filepath] = nil end - on_delete(dir, filepath) - end end @@ -86,7 +72,7 @@ function TreeView:get_cached(dir, item, dirname) -- used only to identify the entry into the cache. local cache_name = item.filename .. (item.topdir and ":" or "") local t = dir_cache[cache_name] - if not t then + if not t or t.type ~= item.type then t = {} local basename = common.basename(item.filename) if item.topdir then @@ -209,10 +195,10 @@ end function TreeView:on_mouse_moved(px, py, ...) if not self.visible then return end - TreeView.super.on_mouse_moved(self, px, py, ...) self.cursor_pos.x = px self.cursor_pos.y = py - if self.dragging_scrollbar then + if TreeView.super.on_mouse_moved(self, px, py, ...) then + -- mouse movement handled by the View (scrollbar) self.hovered_item = nil return end @@ -728,16 +714,8 @@ command.add( end end ) - end -}) + end, - -command.add(function() - if not (core.root_view.overlapping_node and core.root_view.overlapping_node.active_view) then return end - if core.root_view.overlapping_node.active_view ~= view then return end - local item = treeitem() - return item ~= nil, item - end, { ["treeview:rename"] = function(item) local old_filename = item.filename local old_abs_filename = item.abs_filename diff --git a/data/plugins/trimwhitespace.lua b/data/plugins/trimwhitespace.lua index d6057da8..6fb67230 100644 --- a/data/plugins/trimwhitespace.lua +++ b/data/plugins/trimwhitespace.lua @@ -1,10 +1,53 @@ -- mod-version:3 -local core = require "core" +local common = require "core.common" +local config = require "core.config" local command = require "core.command" local Doc = require "core.doc" +---@class config.plugins.trimwhitespace +---@field enabled boolean +---@field trim_empty_end_lines boolean +config.plugins.trimwhitespace = common.merge({ + enabled = true, + trim_empty_end_lines = false, + config_spec = { + name = "Trim Whitespace", + { + label = "Enabled", + description = "Disable or enable the trimming of white spaces by default.", + path = "enabled", + type = "toggle", + default = true + }, + { + label = "Trim Empty End Lines", + description = "Remove any empty new lines at the end of documents.", + path = "trim_empty_end_lines", + type = "toggle", + default = false + } + } +}, config.plugins.trimwhitespace) -local function trim_trailing_whitespace(doc) +---@class plugins.trimwhitespace +local trimwhitespace = {} + +---Disable whitespace trimming for a specific document. +---@param doc core.doc +function trimwhitespace.disable(doc) + doc.disable_trim_whitespace = true +end + +---Re-enable whitespace trimming if previously disabled. +---@param doc core.doc +function trimwhitespace.enable(doc) + doc.disable_trim_whitespace = nil +end + +---Perform whitespace trimming in all lines of a document except the +---line where the caret is currently positioned. +---@param doc core.doc +function trimwhitespace.trim(doc) local cline, ccol = doc:get_selection() for i = 1, #doc.lines do local old_text = doc:get_text(i, 1, i, math.huge) @@ -22,16 +65,54 @@ local function trim_trailing_whitespace(doc) end end +---Removes all empty new lines at the end of the document. +---@param doc core.doc +---@param raw_remove? boolean Perform the removal not registering to undo stack +function trimwhitespace.trim_empty_end_lines(doc, raw_remove) + for _=#doc.lines, 1, -1 do + local l = #doc.lines + if l > 1 and doc.lines[l] == "\n" then + local current_line = doc:get_selection() + if current_line == l then + doc:set_selection(l-1, math.huge, l-1, math.huge) + end + if not raw_remove then + doc:remove(l-1, math.huge, l, math.huge) + else + table.remove(doc.lines, l) + end + else + break + end + end +end + command.add("core.docview", { ["trim-whitespace:trim-trailing-whitespace"] = function(dv) - trim_trailing_whitespace(dv.doc) + trimwhitespace.trim(dv.doc) + end, + + ["trim-whitespace:trim-empty-end-lines"] = function(dv) + trimwhitespace.trim_empty_end_lines(dv.doc) end, }) -local save = Doc.save +local doc_save = Doc.save Doc.save = function(self, ...) - trim_trailing_whitespace(self) - save(self, ...) + if + config.plugins.trimwhitespace.enabled + and + not self.disable_trim_whitespace + then + trimwhitespace.trim(self) + if config.plugins.trimwhitespace.trim_empty_end_lines then + trimwhitespace.trim_empty_end_lines(self) + end + end + doc_save(self, ...) end + + +return trimwhitespace diff --git a/data/plugins/workspace.lua b/data/plugins/workspace.lua index 788a753a..6426cbdb 100644 --- a/data/plugins/workspace.lua +++ b/data/plugins/workspace.lua @@ -92,7 +92,8 @@ local function save_view(view) return { type = "view", active = (core.active_view == view), - module = name + module = name, + scroll = { x = view.scroll.to.x, y = view.scroll.to.y, to = { x = view.scroll.to.x, y = view.scroll.to.y } }, } end end @@ -162,6 +163,9 @@ local function load_node(node, t) if t.active_view == i then active_view = view end + if not view:is(DocView) then + view.scroll = v.scroll + end end end if active_view then diff --git a/docs/api/dirmonitor.lua b/docs/api/dirmonitor.lua new file mode 100644 index 00000000..439e0ec4 --- /dev/null +++ b/docs/api/dirmonitor.lua @@ -0,0 +1,66 @@ +---@meta + +--- +---Functionality that allows to monitor a directory or file for changes +---using the native facilities provided by the current operating system +---for better efficiency and performance. +---@class dirmonitor +dirmonitor = {} + +---@alias dirmonitor.callback fun(fd_or_path:integer|string) + +--- +---Creates a new dirmonitor object. +--- +---@return dirmonitor +function dirmonitor.new() end + +--- +---Monitors a directory or file for changes. +--- +---In "multiple" mode you will need to call this method more than once to +---recursively monitor directories and files. +--- +---In "single" mode you will only need to call this method for the parent +---directory and every sub directory and files will get automatically monitored. +--- +---@param path string +--- +---@return integer fd The file descriptor id assigned to the monitored path when +---the mode is "multiple", in "single" mode: 1 for success or -1 on failure. +function dirmonitor:watch(path) end + +--- +---Stops monitoring a file descriptor in "multiple" mode +---or in "single" mode a directory path. +--- +---@param fd_or_path integer | string A file descriptor or path. +function dirmonitor:unwatch(fd_or_path) end + +--- +---Verify if the resources registered for monitoring have changed, should +---be called periodically to check for changes. +--- +---The callback will be called for each file or directory that was: +---edited, removed or added. A file descriptor will be passed to the +---callback in "multiple" mode or a path in "single" mode. +--- +---@param callback dirmonitor.callback +--- +---@return boolean? changes True when changes were detected. +function dirmonitor:check(callback) end + +--- +---Get the working mode for the current file system monitoring backend. +--- +---"multiple": various file descriptors are needed to recursively monitor a +---directory contents, backends: inotify and kqueue. +--- +---"single": a single process takes care of monitoring a path recursively +---so no individual file descriptors are used, backends: win32 and fsevents. +--- +---@return "single" | "multiple" +function dirmonitor:mode() end + + +return dirmonitor diff --git a/docs/api/globals.lua b/docs/api/globals.lua index b32ff9af..75d290a0 100644 --- a/docs/api/globals.lua +++ b/docs/api/globals.lua @@ -10,7 +10,7 @@ ARGS = {} ARCH = "Architecture-OperatingSystem" ---The current operating system. ----@type string | "'Windows'" | "'Mac OS X'" | "'Linux'" | "'iOS'" | "'Android'" +---@type string | "Windows" | "Mac OS X" | "Linux" | "iOS" | "Android" PLATFORM = "Operating System" ---The current text or ui scale. diff --git a/docs/api/process.lua b/docs/api/process.lua index c384a346..84eafc69 100644 --- a/docs/api/process.lua +++ b/docs/api/process.lua @@ -81,27 +81,27 @@ process.REDIRECT_DISCARD = 3 process.REDIRECT_STDOUT = 4 ---@alias process.errortype ----|>'process.ERROR_PIPE' ----| 'process.ERROR_WOULDBLOCK' ----| 'process.ERROR_TIMEDOUT' ----| 'process.ERROR_INVAL' ----| 'process.ERROR_NOMEM' +---| `process.ERROR_PIPE` +---| `process.ERROR_WOULDBLOCK` +---| `process.ERROR_TIMEDOUT` +---| `process.ERROR_INVAL` +---| `process.ERROR_NOMEM` ---@alias process.streamtype ----|>'process.STREAM_STDIN' ----| 'process.STREAM_STDOUT' ----| 'process.STREAM_STDERR' +---| `process.STREAM_STDIN` +---| `process.STREAM_STDOUT` +---| `process.STREAM_STDERR` ---@alias process.waittype ----|>'process.WAIT_INFINITE' ----| 'process.WAIT_DEADLINE' +---| `process.WAIT_INFINITE` +---| `process.WAIT_DEADLINE` ---@alias process.redirecttype ----|>'process.REDIRECT_DEFAULT' ----| 'process.REDIRECT_PIPE' ----| 'process.REDIRECT_PARENT' ----| 'process.REDIRECT_DISCARD' ----| 'process.REDIRECT_STDOUT' +---| `process.REDIRECT_DEFAULT` +---| `process.REDIRECT_PIPE` +---| `process.REDIRECT_PARENT` +---| `process.REDIRECT_DISCARD` +---| `process.REDIRECT_STDOUT` --- --- Options that can be passed to process.start() @@ -112,7 +112,6 @@ process.REDIRECT_STDOUT = 4 ---@field public stdout process.redirecttype ---@field public stderr process.redirecttype ---@field public env table -process.options = {} --- ---Create and start a new process @@ -233,3 +232,6 @@ function process:returncode() end --- ---@return boolean function process:running() end + + +return process diff --git a/docs/api/regex.lua b/docs/api/regex.lua index 02d8c796..0ff66479 100644 --- a/docs/api/regex.lua +++ b/docs/api/regex.lua @@ -31,9 +31,9 @@ regex.NOTEMPTY = 0x00000004 regex.NOTEMPTY_ATSTART = 0x00000008 ---@alias regex.modifiers ----|>'"i"' # Case insesitive matching ----| '"m"' # Multiline matching ----| '"s"' # Match all characters with dot (.) metacharacter even new lines +---| "i" # Case insesitive matching +---| "m" # Multiline matching +---| "s" # Match all characters with dot (.) metacharacter even new lines --- ---Compiles a regular expression pattern that can be used to search in strings. @@ -41,8 +41,8 @@ regex.NOTEMPTY_ATSTART = 0x00000008 ---@param pattern string ---@param options? regex.modifiers A string of one or more pattern modifiers. --- ----@return regex|string regex Ready to use regular expression object or error ----message if compiling the pattern failed. +---@return regex? regex Ready to use regular expression object or nil on error. +---@return string? error The error message if compiling the pattern failed. function regex.compile(pattern, options) end --- @@ -53,5 +53,42 @@ function regex.compile(pattern, options) end ---@param options? integer A bit field of matching options, eg: ---regex.NOTBOL | regex.NOTEMPTY --- ----@return table list List of offsets where a match was found. +---@return integer? ... List of offsets where a match was found. function regex:cmatch(subject, offset, options) end + +--- +---Returns an iterator function that, each time it is called, returns the +---next captures from `pattern` over the string subject. +--- +---Example: +---```lua +--- s = "hello world hello world" +--- for hello, world in regex.gmatch("(hello)\\s+(world)", s) do +--- print(hello .. " " .. world) +--- end +---``` +--- +---@param pattern string +---@param subject string +---@param offset? integer +--- +---@return fun():string, ... +function regex.gmatch(pattern, subject, offset) end + +--- +---Replaces the matched pattern globally on the subject with the given +---replacement, supports named captures ((?'name'), ${name}) and +---$[1-9][0-9]* substitutions. Raises an error when failing to compile the +---pattern or by a substitution mistake. +--- +---@param pattern regex|string +---@param subject string +---@param replacement string +---@param limit? integer Limits the number of substitutions that will be done. +--- +---@return string? replaced_subject +---@return integer? total_replacements +function regex.gsub(pattern, subject, replacement, limit) end + + +return regex diff --git a/docs/api/renderer.lua b/docs/api/renderer.lua index fe42ed65..e912d645 100644 --- a/docs/api/renderer.lua +++ b/docs/api/renderer.lua @@ -14,19 +14,17 @@ renderer = {} ---@field public g number Green ---@field public b number Blue ---@field public a number Alpha -renderer.color = {} --- ---Represent options that affect a font's rendering. ---@class renderer.fontoptions ----@field public antialiasing "'none'" | "'grayscale'" | "'subpixel'" ----@field public hinting "'slight'" | "'none'" | '"full"' --- @field public bold boolean --- @field public italic boolean --- @field public underline boolean --- @field public smoothing boolean --- @field public strikethrough boolean -renderer.fontoptions = {} +---@field public antialiasing "none" | "grayscale" | "subpixel" +---@field public hinting "slight" | "none" | "full" +---@field public bold boolean +---@field public italic boolean +---@field public underline boolean +---@field public smoothing boolean +---@field public strikethrough boolean --- ---@class renderer.font @@ -154,3 +152,6 @@ function renderer.draw_rect(x, y, width, height, color) end --- ---@return number x function renderer.draw_text(font, text, x, y, color) end + + +return renderer diff --git a/docs/api/string.lua b/docs/api/string.lua index 0872b462..9ac07510 100644 --- a/docs/api/string.lua +++ b/docs/api/string.lua @@ -101,14 +101,14 @@ function string.unext(s, charpos, index) end ---@param s string ---@param idx? integer ---@param substring string ----return string new_string +---@return string new_string function string.uinsert(s, idx, substring) end ---Equivalent to utf8.remove() ---@param s string ---@param start? integer ---@param stop? integer ----return string new_string +---@return string new_string function string.uremove(s, start, stop) end ---Equivalent to utf8.width() @@ -130,12 +130,12 @@ function string.uwidthindex(s, location, ambi_is_double, default_width) end ---Equivalent to utf8.title() ---@param s string ----return string new_string +---@return string new_string function string.utitle(s) end ---Equivalent to utf8.fold() ---@param s string ----return string new_string +---@return string new_string function string.ufold(s) end ---Equivalent to utf8.ncasecmp() diff --git a/docs/api/system.lua b/docs/api/system.lua index e5eff27f..17c379fa 100644 --- a/docs/api/system.lua +++ b/docs/api/system.lua @@ -6,15 +6,15 @@ system = {} ---@alias system.fileinfotype ----|>'"file"' # It is a file. ----| '"dir"' # It is a directory. +---| "file" # It is a file. +---| "dir" # It is a directory. --- ---@class system.fileinfo ---@field public modified number A timestamp in seconds. ---@field public size number Size in bytes. ---@field public type system.fileinfotype Type of file -system.fileinfo = {} +---@field public symlink boolean The directory is a symlink. This field is only set on Linux and on directories. --- ---Core function used to retrieve the current event been triggered by SDL. @@ -24,7 +24,7 @@ system.fileinfo = {} --- ---Window events: --- * "quit" ---- * "resized" -> width, height +--- * "resized" -> width, height (in points) --- * "exposed" --- * "minimized" --- * "maximized" @@ -38,12 +38,18 @@ system.fileinfo = {} --- * "keypressed" -> key_name --- * "keyreleased" -> key_name --- * "textinput" -> text +--- * "textediting" -> text, start, length --- ---Mouse events: --- * "mousepressed" -> button_name, x, y, amount_of_clicks --- * "mousereleased" -> button_name, x, y --- * "mousemoved" -> x, y, relative_x, relative_y ---- * "mousewheel" -> y +--- * "mousewheel" -> y, x +--- +---Touch events: +--- * "touchpressed" -> x, y, finger_id +--- * "touchreleased" -> x, y, finger_id +--- * "touchmoved" -> x, y, distance_x, distance_y, finger_id --- ---@return string type ---@return any? arg1 @@ -64,7 +70,7 @@ function system.wait_event(timeout) end --- ---Change the cursor type displayed on screen. --- ----@param type string | "'arrow'" | "'ibeam'" | "'sizeh'" | "'sizev'" | "'hand'" +---@param type string | "arrow" | "ibeam" | "sizeh" | "sizev" | "hand" function system.set_cursor(type) end --- @@ -74,10 +80,10 @@ function system.set_cursor(type) end function system.set_window_title(title) end ---@alias system.windowmode ----|>'"normal"' ----| '"minimized"' ----| '"maximized"' ----| '"fullscreen"' +---| "normal" +---| "minimized" +---| "maximized" +---| "fullscreen" --- ---Change the window mode. @@ -103,7 +109,7 @@ function system.set_window_bordered(bordered) end ---for custom window management. --- ---@param title_height number ----@param controls_width number This is for minimize, maximize, close, etc... +---@param controls_width number Width of window controls (maximize,minimize and close buttons, etc). ---@param resize_border number The amount of pixels reserved for resizing function system.set_window_hit_test(title_height, controls_width, resize_border) end @@ -131,6 +137,30 @@ function system.set_window_size(width, height, x, y) end ---@return boolean function system.window_has_focus() end +--- +---Gets the mode of the window. +--- +---@return system.windowmode +function system.get_window_mode() end + +--- +---Sets the position of the IME composition window. +--- +---@param x number +---@param y number +---@param width number +---@param height number +function system.set_text_input_rect(x, y, width, height) end + +--- +---Clears any ongoing composition on the IME +function system.clear_ime() end + +--- +---Raise the main window and give it input focus. +---Note: may not always be obeyed by the users window manager. +function system.raise_window() end + --- ---Opens a message box to display an error message. --- @@ -138,6 +168,14 @@ function system.window_has_focus() end ---@param message string function system.show_fatal_error(title, message) end +--- +---Deletes an empty directory. +--- +---@param path string +---@return boolean success True if the operation suceeded, false otherwise +---@return string? message An error message if the operation failed +function system.rmdir(path) end + --- ---Change the current directory path which affects relative file operations. ---This function raises an error if the path doesn't exists. @@ -152,6 +190,7 @@ function system.chdir(path) end ---@param directory_path string --- ---@return boolean created True on success or false on failure. +---@return string? message The error message if the operation failed. function system.mkdir(directory_path) end --- @@ -168,7 +207,7 @@ function system.list_dir(path) end --- ---@param path string --- ----@return string +---@return string? abspath function system.absolute_path(path) end --- @@ -180,6 +219,28 @@ function system.absolute_path(path) end ---@return string? message Error message in case of error. function system.get_file_info(path) end + +---@alias system.fstype +---| "ext2/ext3" +---| "nfs" +---| "fuse" +---| "smb" +---| "smb2" +---| "reiserfs" +---| "tmpfs" +---| "ramfs" +---| "ntfs" + +--- +---Gets the filesystem type of a path. +---Note: This only works on Linux. +--- +---@param path string Can be path to a directory or a file +--- +---@return system.fstype +function system.get_fs_type(path) end + + --- ---Retrieve the text currently stored on the clipboard. --- @@ -193,7 +254,7 @@ function system.get_clipboard() end function system.set_clipboard(text) end --- ----Get the process id of lite-xl it self. +---Get the process id of lite-xl itself. --- ---@return integer function system.get_process_id() end @@ -215,7 +276,9 @@ function system.sleep(seconds) end ---Similar to os.execute() but does not return the exit status of the ---executed command and executes the process in a non blocking way by ---forking it to the background. +---Note: Do not use this function, use the Process API instead. --- +---@deprecated ---@param command string The command to execute. function system.exec(command) end @@ -237,4 +300,25 @@ function system.fuzzy_match(haystack, needle, file) end --- ---@param opacity number A value from 0.0 to 1.0, the lower the value ---the less visible the window will be. +---@return boolean success True if the operation suceeded. function system.set_window_opacity(opacity) end + +--- +---Loads a lua native module using the default Lua API or lite-xl native plugin API. +---Note: Never use this function directly. +--- +---@param name string the name of the module +---@param path string the path to the shared library file +---@return number nargs the return value of the entrypoint +function system.load_native_plugin(name, path) end + +--- +---Compares two paths in the order used by TreeView. +--- +---@param path1 string +---@param path2 string +---@return boolean compare_result True if path1 < path2 +function system.path_compare(path1, path2) end + + +return system diff --git a/docs/api/utf8extra.lua b/docs/api/utf8extra.lua index 1ff4dcb6..ae9a440e 100644 --- a/docs/api/utf8extra.lua +++ b/docs/api/utf8extra.lua @@ -131,7 +131,7 @@ function utf8extra.next(s, charpos, index) end ---@param s string ---@param idx? integer ---@param substring string ----return string new_string +---@return string new_string function utf8extra.insert(s, idx, substring) end ---Delete a substring in s. If neither start nor stop is given, delete the last @@ -141,7 +141,7 @@ function utf8extra.insert(s, idx, substring) end ---@param s string ---@param start? integer ---@param stop? integer ----return string new_string +---@return string new_string function utf8extra.remove(s, start, stop) end ---Calculate the width of UTF-8 string s. if ambi_is_double is given, the @@ -174,14 +174,14 @@ function utf8extra.widthindex(s, location, ambi_is_double, default_width) end ---is a number, it's treat as a code point and return a convert code point ---(number). utf8.lower/utf8.pper has the same extension. ---@param s string ----return string new_string +---@return string new_string function utf8extra.title(s) end ---Convert UTF-8 string s to folded case, used to compare by ignore case. if s ---is a number, it's treat as a code point and return a convert code point ---(number). utf8.lower/utf8.pper has the same extension. ---@param s string ----return string new_string +---@return string new_string function utf8extra.fold(s) end ---Compare a and b without case, -1 means a < b, 0 means a == b and 1 means a > b. @@ -189,3 +189,6 @@ function utf8extra.fold(s) end ---@param b string ---@return integer result function utf8extra.ncasecmp(a, b) end + + +return utf8extra diff --git a/meson.build b/meson.build index 9454fceb..e5fc1fbe 100644 --- a/meson.build +++ b/meson.build @@ -1,8 +1,8 @@ project('lite-xl', ['c'], - version : '2.1.0', + version : '2.1.1', license : 'MIT', - meson_version : '>= 0.47', + meson_version : '>= 0.56', default_options : [ 'c_std=gnu11', 'wrap_mode=nofallback' @@ -52,6 +52,13 @@ lite_cargs = ['-DSDL_MAIN_HANDLED', '-DPCRE2_STATIC'] if get_option('renderer') or host_machine.system() == 'darwin' lite_cargs += '-DLITE_USE_SDL_RENDERER' endif +if get_option('arch_tuple') != '' + arch_tuple = get_option('arch_tuple') +else + arch_tuple = '@0@-@1@'.format(target_machine.cpu_family(), target_machine.system()) +endif +lite_cargs += '-DLITE_ARCH_TUPLE="@0@"'.format(arch_tuple) + #=============================================================================== # Linker Settings #=============================================================================== @@ -82,13 +89,20 @@ if not get_option('source-only') foreach lua : lua_names last_lua = (lua == lua_names[-1] or get_option('wrap_mode') == 'forcefallback') - lua_dep = dependency(lua, fallback: last_lua ? ['lua', 'lua_dep'] : [], required : last_lua, + 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'] ) if lua_dep.found() break endif + + if last_lua + # If we could not find lua on the system and fallbacks are disabled + # try the compiler as a last ditch effort, since Lua has no official + # pkg-config support. + lua_dep = cc.find_library('lua', required : true) + endif endforeach pcre2_dep = dependency('libpcre2-8', fallback: ['pcre2', 'libpcre2_8'], diff --git a/meson_options.txt b/meson_options.txt index 7850416e..9cfcc353 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -2,4 +2,5 @@ option('bundle', type : 'boolean', value : false, description: 'Build a macOS bu option('source-only', type : 'boolean', value : false, description: 'Configure source files only, doesn\'t checks for dependencies') option('portable', type : 'boolean', value : false, description: 'Portable install') option('renderer', type : 'boolean', value : false, description: 'Use SDL renderer') -option('dirmonitor_backend', type : 'combo', value : '', choices : ['', 'inotify', 'kqueue', 'win32', 'dummy'], description: 'define what dirmonitor backend to use') \ No newline at end of file +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') \ No newline at end of file diff --git a/resources/linux/org.lite_xl.lite_xl.appdata.xml b/resources/linux/org.lite_xl.lite_xl.appdata.xml index 1932af90..b707ebe1 100644 --- a/resources/linux/org.lite_xl.lite_xl.appdata.xml +++ b/resources/linux/org.lite_xl.lite_xl.appdata.xml @@ -29,6 +29,6 @@ - + diff --git a/resources/linux/org.lite_xl.lite_xl.desktop b/resources/linux/org.lite_xl.lite_xl.desktop index d251c4dc..77c98415 100644 --- a/resources/linux/org.lite_xl.lite_xl.desktop +++ b/resources/linux/org.lite_xl.lite_xl.desktop @@ -7,4 +7,4 @@ Icon=lite-xl Terminal=false StartupWMClass=lite-xl Categories=Development;IDE; -MimeType=text/plain; +MimeType=text/plain;inode/directory; diff --git a/resources/lite_xl_plugin_api.h b/resources/lite_xl_plugin_api.h index 285720c4..5337d8ae 100644 --- a/resources/lite_xl_plugin_api.h +++ b/resources/lite_xl_plugin_api.h @@ -4,7 +4,7 @@ The lite_xl plugin API is quite simple. Any shared library can be a plugin file, so long as it has an entrypoint that looks like the following, where xxxxx is the plugin name: #include "lite_xl_plugin_api.h" -int lua_open_lite_xl_xxxxx(lua_State* L, void* XL) { +int luaopen_lite_xl_xxxxx(lua_State* L, void* XL) { lite_xl_plugin_init(XL); ... return 1; @@ -12,6 +12,11 @@ int lua_open_lite_xl_xxxxx(lua_State* L, void* XL) { In linux, to compile this file, you'd do: 'gcc -o xxxxx.so -shared xxxxx.c'. Simple! Due to the way the API is structured, you *should not* link or include lua libraries. This file was automatically generated. DO NOT MODIFY DIRECTLY. + +UNLESS you're us, and you had to modify this file manually to get it ready for 2.1. + +Go figure. + **/ @@ -231,9 +236,6 @@ static int (*lua_absindex) (lua_State *L, int idx); static int (*lua_gettop) (lua_State *L); static void (*lua_settop) (lua_State *L, int idx); static void (*lua_pushvalue) (lua_State *L, int idx); -static void (*lua_remove) (lua_State *L, int idx); -static void (*lua_insert) (lua_State *L, int idx); -static void (*lua_replace) (lua_State *L, int idx); static void (*lua_copy) (lua_State *L, int fromidx, int toidx); static int (*lua_checkstack) (lua_State *L, int sz); static void (*lua_xmove) (lua_State *from, lua_State *to, int n); @@ -276,8 +278,10 @@ static void (*lua_rawgeti) (lua_State *L, int idx, int n); static void (*lua_rawgetp) (lua_State *L, int idx, const void *p); static void (*lua_createtable) (lua_State *L, int narr, int nrec); static void *(*lua_newuserdata) (lua_State *L, size_t sz); +static void *(*lua_newuserdatauv) (lua_State *L, size_t sz, int nuvalue); static int (*lua_getmetatable) (lua_State *L, int objindex); static void (*lua_getuservalue) (lua_State *L, int idx); +static void (*lua_getiuservalue) (lua_State *L, int idx, int n); static void (*lua_setglobal) (lua_State *L, const char *var); static void (*lua_settable) (lua_State *L, int idx); static void (*lua_setfield) (lua_State *L, int idx, const char *k); @@ -286,11 +290,12 @@ static void (*lua_rawseti) (lua_State *L, int idx, int n); static void (*lua_rawsetp) (lua_State *L, int idx, const void *p); static int (*lua_setmetatable) (lua_State *L, int objindex); static void (*lua_setuservalue) (lua_State *L, int idx); +static void (*lua_setiuservalue) (lua_State *L, int idx, int n); static void (*lua_callk) (lua_State *L, int nargs, int nresults, int ctx, lua_CFunction k); static int (*lua_getctx) (lua_State *L, int *ctx); static int (*lua_pcallk) (lua_State *L, int nargs, int nresults, int errfunc, int ctx, lua_CFunction k); static int (*lua_load) (lua_State *L, lua_Reader reader, void *dt, const char *chunkname, const char *mode); -static int (*lua_dump) (lua_State *L, lua_Writer writer, void *data); +static int (*lua_dump) (lua_State *L, lua_Writer writer, void *data, int strip); static int (*lua_yieldk) (lua_State *L, int nresults, int ctx, lua_CFunction k); static int (*lua_resume) (lua_State *L, lua_State *from, int narg); static int (*lua_status) (lua_State *L); @@ -391,6 +396,9 @@ static int (*lua_gethookcount) (lua_State *L); #define lua_pushliteral(L, s) lua_pushlstring(L, "" s, (sizeof(s)/sizeof(char))-1) #define lua_pushglobaltable(L) lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS) #define lua_tostring(L,i) lua_tolstring(L, (i), NULL) +#define lua_insert(L,idx) lua_rotate(L, (idx), 1) +#define lua_replace(L,idx) (lua_copy(L, -1, (idx)), lua_pop(L, 1)) +#define lua_remove(L,idx) (lua_rotate(L, (idx), -1), lua_pop(L, 1)) #define LUA_HOOKCALL 0 #define LUA_HOOKRET 1 #define LUA_HOOKLINE 2 @@ -409,9 +417,6 @@ static int __lite_xl_fallback_lua_absindex (lua_State *L, int idx) { fputs("war static int __lite_xl_fallback_lua_gettop (lua_State *L) { fputs("warning: lua_gettop is a stub", stderr); } static void __lite_xl_fallback_lua_settop (lua_State *L, int idx) { fputs("warning: lua_settop is a stub", stderr); } static void __lite_xl_fallback_lua_pushvalue (lua_State *L, int idx) { fputs("warning: lua_pushvalue is a stub", stderr); } -static void __lite_xl_fallback_lua_remove (lua_State *L, int idx) { fputs("warning: lua_remove is a stub", stderr); } -static void __lite_xl_fallback_lua_insert (lua_State *L, int idx) { fputs("warning: lua_insert is a stub", stderr); } -static void __lite_xl_fallback_lua_replace (lua_State *L, int idx) { fputs("warning: lua_replace is a stub", stderr); } static void __lite_xl_fallback_lua_copy (lua_State *L, int fromidx, int toidx) { fputs("warning: lua_copy is a stub", stderr); } static int __lite_xl_fallback_lua_checkstack (lua_State *L, int sz) { fputs("warning: lua_checkstack is a stub", stderr); } static void __lite_xl_fallback_lua_xmove (lua_State *from, lua_State *to, int n) { fputs("warning: lua_xmove is a stub", stderr); } @@ -454,8 +459,10 @@ static void __lite_xl_fallback_lua_rawgeti (lua_State *L, int idx, int n) { fpu static void __lite_xl_fallback_lua_rawgetp (lua_State *L, int idx, const void *p) { fputs("warning: lua_rawgetp is a stub", stderr); } static void __lite_xl_fallback_lua_createtable (lua_State *L, int narr, int nrec) { fputs("warning: lua_createtable is a stub", stderr); } static void * __lite_xl_fallback_lua_newuserdata (lua_State *L, size_t sz) { fputs("warning: lua_newuserdata is a stub", stderr); } +static void * __lite_xl_fallback_lua_newuserdatauv (lua_State *L, size_t sz, int nuvalue) { fputs("warning: lua_newuserdatauv is a stub", stderr); } static int __lite_xl_fallback_lua_getmetatable (lua_State *L, int objindex) { fputs("warning: lua_getmetatable is a stub", stderr); } static void __lite_xl_fallback_lua_getuservalue (lua_State *L, int idx) { fputs("warning: lua_getuservalue is a stub", stderr); } +static void __lite_xl_fallback_lua_getiuservalue (lua_State *L, int idx, int n) { fputs("warning: lua_getiuservalue is a stub", stderr); } static void __lite_xl_fallback_lua_setglobal (lua_State *L, const char *var) { fputs("warning: lua_setglobal is a stub", stderr); } static void __lite_xl_fallback_lua_settable (lua_State *L, int idx) { fputs("warning: lua_settable is a stub", stderr); } static void __lite_xl_fallback_lua_setfield (lua_State *L, int idx, const char *k) { fputs("warning: lua_setfield is a stub", stderr); } @@ -464,11 +471,12 @@ static void __lite_xl_fallback_lua_rawseti (lua_State *L, int idx, int n) { fpu static void __lite_xl_fallback_lua_rawsetp (lua_State *L, int idx, const void *p) { fputs("warning: lua_rawsetp is a stub", stderr); } static int __lite_xl_fallback_lua_setmetatable (lua_State *L, int objindex) { fputs("warning: lua_setmetatable is a stub", stderr); } static void __lite_xl_fallback_lua_setuservalue (lua_State *L, int idx) { fputs("warning: lua_setuservalue is a stub", stderr); } +static void __lite_xl_fallback_lua_setiuservalue (lua_State *L, int idx, int n) { fputs("warning: lua_setiuservalue is a stub", stderr); } static void __lite_xl_fallback_lua_callk (lua_State *L, int nargs, int nresults, int ctx, lua_CFunction k) { fputs("warning: lua_callk is a stub", stderr); } static int __lite_xl_fallback_lua_getctx (lua_State *L, int *ctx) { fputs("warning: lua_getctx is a stub", stderr); } static int __lite_xl_fallback_lua_pcallk (lua_State *L, int nargs, int nresults, int errfunc, int ctx, lua_CFunction k) { fputs("warning: lua_pcallk is a stub", stderr); } static int __lite_xl_fallback_lua_load (lua_State *L, lua_Reader reader, void *dt, const char *chunkname, const char *mode) { fputs("warning: lua_load is a stub", stderr); } -static int __lite_xl_fallback_lua_dump (lua_State *L, lua_Writer writer, void *data) { fputs("warning: lua_dump is a stub", stderr); } +static int __lite_xl_fallback_lua_dump (lua_State *L, lua_Writer writer, void *data, int strip) { fputs("warning: lua_dump is a stub", stderr); } static int __lite_xl_fallback_lua_yieldk (lua_State *L, int nresults, int ctx, lua_CFunction k) { fputs("warning: lua_yieldk is a stub", stderr); } static int __lite_xl_fallback_lua_resume (lua_State *L, lua_State *from, int narg) { fputs("warning: lua_resume is a stub", stderr); } static int __lite_xl_fallback_lua_status (lua_State *L) { fputs("warning: lua_status is a stub", stderr); } @@ -492,6 +500,7 @@ static lua_Hook __lite_xl_fallback_lua_gethook (lua_State *L) { fputs("warning: static int __lite_xl_fallback_lua_gethookmask (lua_State *L) { fputs("warning: lua_gethookmask is a stub", stderr); } static int __lite_xl_fallback_lua_gethookcount (lua_State *L) { fputs("warning: lua_gethookcount is a stub", stderr); } + /** lauxlib.h **/ typedef struct luaL_Reg { @@ -531,6 +540,7 @@ static void *(*luaL_testudata) (lua_State *L, int ud, const char *tname); static void *(*luaL_checkudata) (lua_State *L, int ud, const char *tname); static void (*luaL_where) (lua_State *L, int lvl); static int (*luaL_error) (lua_State *L, const char *fmt, ...); +static int (*luaL_typeerror) (lua_State *L, int narg, const char *tname); static int (*luaL_checkoption) (lua_State *L, int narg, const char *def, const char *const lst[]); static int (*luaL_fileresult) (lua_State *L, int stat, const char *fname); static int (*luaL_execresult) (lua_State *L, int stat); @@ -554,6 +564,7 @@ static void (*luaL_addvalue) (luaL_Buffer *B); static void (*luaL_pushresult) (luaL_Buffer *B); static void (*luaL_pushresultsize) (luaL_Buffer *B, size_t sz); static char *(*luaL_buffinitsize) (lua_State *L, luaL_Buffer *B, size_t sz); +static void (*luaL_openlibs) (lua_State *L); #define lauxlib_h #define LUA_ERRFILE (LUA_ERRERR+1) #define luaL_checkversion(L) luaL_checkversion_(L, LUA_VERSION_NUM) @@ -601,6 +612,7 @@ static void * __lite_xl_fallback_luaL_testudata (lua_State *L, int ud, const cha static void * __lite_xl_fallback_luaL_checkudata (lua_State *L, int ud, const char *tname) { fputs("warning: luaL_checkudata is a stub", stderr); } static void __lite_xl_fallback_luaL_where (lua_State *L, int lvl) { fputs("warning: luaL_where is a stub", stderr); } static int __lite_xl_fallback_luaL_error (lua_State *L, const char *fmt, ...) { fputs("warning: luaL_error is a stub", stderr); } +static int __lite_xl_fallback_luaL_typeerror (lua_State *L, int narg, const char *tname) { fputs("warning: luaL_typeerror is a stub", stderr); } static int __lite_xl_fallback_luaL_checkoption (lua_State *L, int narg, const char *def, const char *const lst[]) { fputs("warning: luaL_checkoption is a stub", stderr); } static int __lite_xl_fallback_luaL_fileresult (lua_State *L, int stat, const char *fname) { fputs("warning: luaL_fileresult is a stub", stderr); } static int __lite_xl_fallback_luaL_execresult (lua_State *L, int stat) { fputs("warning: luaL_execresult is a stub", stderr); } @@ -624,6 +636,7 @@ static void __lite_xl_fallback_luaL_addvalue (luaL_Buffer *B) { fputs("warning: static void __lite_xl_fallback_luaL_pushresult (luaL_Buffer *B) { fputs("warning: luaL_pushresult is a stub", stderr); } static void __lite_xl_fallback_luaL_pushresultsize (luaL_Buffer *B, size_t sz) { fputs("warning: luaL_pushresultsize is a stub", stderr); } static char * __lite_xl_fallback_luaL_buffinitsize (lua_State *L, luaL_Buffer *B, size_t sz) { fputs("warning: luaL_buffinitsize is a stub", stderr); } +static void __lite_xl_fallback_luaL_openlibs (lua_State *L) { fputs("warning: luaL_openlibs is a stub", stderr); } #define IMPORT_SYMBOL(name, ret, ...) name = (name = (ret (*) (__VA_ARGS__)) symbol(#name), name == NULL ? &__lite_xl_fallback_##name : name) static void lite_xl_plugin_init(void *XL) { @@ -637,9 +650,6 @@ static void lite_xl_plugin_init(void *XL) { IMPORT_SYMBOL(lua_gettop, int , lua_State *L); IMPORT_SYMBOL(lua_settop, void , lua_State *L, int idx); IMPORT_SYMBOL(lua_pushvalue, void , lua_State *L, int idx); - IMPORT_SYMBOL(lua_remove, void , lua_State *L, int idx); - IMPORT_SYMBOL(lua_insert, void , lua_State *L, int idx); - IMPORT_SYMBOL(lua_replace, void , lua_State *L, int idx); IMPORT_SYMBOL(lua_copy, void , lua_State *L, int fromidx, int toidx); IMPORT_SYMBOL(lua_checkstack, int , lua_State *L, int sz); IMPORT_SYMBOL(lua_xmove, void , lua_State *from, lua_State *to, int n); @@ -682,8 +692,10 @@ static void lite_xl_plugin_init(void *XL) { IMPORT_SYMBOL(lua_rawgetp, void , lua_State *L, int idx, const void *p); IMPORT_SYMBOL(lua_createtable, void , lua_State *L, int narr, int nrec); IMPORT_SYMBOL(lua_newuserdata, void *, lua_State *L, size_t sz); + IMPORT_SYMBOL(lua_newuserdatauv, void *, lua_State *L, size_t sz, int nuvalue); IMPORT_SYMBOL(lua_getmetatable, int , lua_State *L, int objindex); IMPORT_SYMBOL(lua_getuservalue, void , lua_State *L, int idx); + IMPORT_SYMBOL(lua_getiuservalue, void , lua_State *L, int idx, int n); IMPORT_SYMBOL(lua_setglobal, void , lua_State *L, const char *var); IMPORT_SYMBOL(lua_settable, void , lua_State *L, int idx); IMPORT_SYMBOL(lua_setfield, void , lua_State *L, int idx, const char *k); @@ -692,11 +704,12 @@ static void lite_xl_plugin_init(void *XL) { IMPORT_SYMBOL(lua_rawsetp, void , lua_State *L, int idx, const void *p); IMPORT_SYMBOL(lua_setmetatable, int , lua_State *L, int objindex); IMPORT_SYMBOL(lua_setuservalue, void , lua_State *L, int idx); + IMPORT_SYMBOL(lua_setiuservalue, void , lua_State *L, int idx, int n); IMPORT_SYMBOL(lua_callk, void , lua_State *L, int nargs, int nresults, int ctx, lua_CFunction k); IMPORT_SYMBOL(lua_getctx, int , lua_State *L, int *ctx); IMPORT_SYMBOL(lua_pcallk, int , lua_State *L, int nargs, int nresults, int errfunc, int ctx, lua_CFunction k); IMPORT_SYMBOL(lua_load, int , lua_State *L, lua_Reader reader, void *dt, const char *chunkname, const char *mode); - IMPORT_SYMBOL(lua_dump, int , lua_State *L, lua_Writer writer, void *data); + IMPORT_SYMBOL(lua_dump, int , lua_State *L, lua_Writer writer, void *data, int strip); IMPORT_SYMBOL(lua_yieldk, int , lua_State *L, int nresults, int ctx, lua_CFunction k); IMPORT_SYMBOL(lua_resume, int , lua_State *L, lua_State *from, int narg); IMPORT_SYMBOL(lua_status, int , lua_State *L); @@ -741,6 +754,7 @@ static void lite_xl_plugin_init(void *XL) { IMPORT_SYMBOL(luaL_checkudata, void *, lua_State *L, int ud, const char *tname); IMPORT_SYMBOL(luaL_where, void , lua_State *L, int lvl); IMPORT_SYMBOL(luaL_error, int , lua_State *L, const char *fmt, ...); + IMPORT_SYMBOL(luaL_typeerror, int , lua_State *L, int narg, const char *tname); IMPORT_SYMBOL(luaL_checkoption, int , lua_State *L, int narg, const char *def, const char *const lst[]); IMPORT_SYMBOL(luaL_fileresult, int , lua_State *L, int stat, const char *fname); IMPORT_SYMBOL(luaL_execresult, int , lua_State *L, int stat); @@ -764,5 +778,6 @@ static void lite_xl_plugin_init(void *XL) { IMPORT_SYMBOL(luaL_pushresult, void , luaL_Buffer *B); IMPORT_SYMBOL(luaL_pushresultsize, void , luaL_Buffer *B, size_t sz); IMPORT_SYMBOL(luaL_buffinitsize, char *, lua_State *L, luaL_Buffer *B, size_t sz); + IMPORT_SYMBOL(luaL_openlibs, void, lua_State* L); } #endif diff --git a/resources/macos/Info.plist.in b/resources/macos/Info.plist.in index 2cb6208e..6fe6b751 100644 --- a/resources/macos/Info.plist.in +++ b/resources/macos/Info.plist.in @@ -27,7 +27,7 @@ CFBundleShortVersionString @PROJECT_VERSION@ NSHumanReadableCopyright - © 2019-2021 Francesco Abbate + © 2019-2022 Lite XL Team - + diff --git a/resources/macos/macos_arm64.conf b/resources/macos/macos_arm64.conf new file mode 100644 index 00000000..b1371b3b --- /dev/null +++ b/resources/macos/macos_arm64.conf @@ -0,0 +1,24 @@ +[host_machine] +system = 'darwin' +cpu_family = 'aarch64' +cpu = 'arm64' +endian = 'little' + +[binaries] +c = ['clang'] +cpp = ['clang++'] +objc = ['clang'] +objcpp = ['clang++'] +ar = ['ar'] +strip = ['strip'] +pkgconfig = ['pkg-config'] + +[built-in options] +c_args = ['-arch', 'arm64'] +cpp_args = ['-stdlib=libc++', '-arch', 'arm64'] +objc_args = ['-arch', 'arm64'] +objcpp_args = ['-stdlib=libc++', '-arch', 'arm64'] +c_link_args = ['-arch', 'arm64'] +cpp_link_args = ['-arch', 'arm64'] +objc_link_args = ['-arch', 'arm64'] +objcpp_link_args = ['-arch', 'arm64'] diff --git a/resources/windows/001-lua-unicode.diff b/resources/windows/001-lua-unicode.diff index 8306d354..7785b8ea 100644 --- a/resources/windows/001-lua-unicode.diff +++ b/resources/windows/001-lua-unicode.diff @@ -1,22 +1,21 @@ -diff -ruN lua-5.4.3/meson.build newlua/meson.build ---- lua-5.4.3/meson.build 2022-05-29 21:04:17.850449500 +0800 -+++ newlua/meson.build 2022-06-10 19:23:55.685139800 +0800 -@@ -82,6 +82,7 @@ +diff -ruN lua-5.4.4/meson.build lua-5.4.4-mod/meson.build +--- lua-5.4.4/meson.build 2022-11-16 10:33:38.424383300 +0800 ++++ lua-5.4.4-mod/meson.build 2022-11-16 09:40:57.697918000 +0800 +@@ -85,6 +85,7 @@ 'src/lutf8lib.c', 'src/lvm.c', 'src/lzio.c', + 'src/utf8_wrappers.c', dependencies: lua_lib_deps, - override_options: project_options, - implicit_include_directories: false, -Binary files lua-5.4.3/src/lua54.dll and newlua/src/lua54.dll differ -diff -ruN lua-5.4.3/src/luaconf.h newlua/src/luaconf.h ---- lua-5.4.3/src/luaconf.h 2021-03-15 21:32:52.000000000 +0800 -+++ newlua/src/luaconf.h 2022-06-10 19:15:03.014745300 +0800 -@@ -786,5 +786,15 @@ - - - + version: meson.project_version(), + soversion: lua_versions[0] + '.' + lua_versions[1], +diff -ruN lua-5.4.4/src/luaconf.h lua-5.4.4-mod/src/luaconf.h +--- lua-5.4.4/src/luaconf.h 2022-01-13 19:24:43.000000000 +0800 ++++ lua-5.4.4-mod/src/luaconf.h 2022-11-16 09:40:57.703926000 +0800 +@@ -782,5 +782,15 @@ + + + +#if defined(lua_c) || defined(luac_c) || (defined(LUA_LIB) && \ + (defined(lauxlib_c) || defined(liolib_c) || \ + defined(loadlib_c) || defined(loslib_c))) @@ -28,23 +27,22 @@ diff -ruN lua-5.4.3/src/luaconf.h newlua/src/luaconf.h + + #endif - -diff -ruN lua-5.4.3/src/Makefile newlua/src/Makefile ---- lua-5.4.3/src/Makefile 2021-02-10 02:47:17.000000000 +0800 -+++ newlua/src/Makefile 2022-06-10 19:22:45.267931400 +0800 + +diff -ruN lua-5.4.4/src/Makefile lua-5.4.4-mod/src/Makefile +--- lua-5.4.4/src/Makefile 2021-07-15 22:01:52.000000000 +0800 ++++ lua-5.4.4-mod/src/Makefile 2022-11-16 09:40:57.708921800 +0800 @@ -33,7 +33,7 @@ PLATS= guess aix bsd c89 freebsd generic linux linux-readline macosx mingw posix solaris - + LUA_A= liblua.a -CORE_O= lapi.o lcode.o lctype.o ldebug.o ldo.o ldump.o lfunc.o lgc.o llex.o lmem.o lobject.o lopcodes.o lparser.o lstate.o lstring.o ltable.o ltm.o lundump.o lvm.o lzio.o +CORE_O= lapi.o lcode.o lctype.o ldebug.o ldo.o ldump.o lfunc.o lgc.o llex.o lmem.o lobject.o lopcodes.o lparser.o lstate.o lstring.o ltable.o ltm.o lundump.o lvm.o lzio.o utf8_wrappers.o LIB_O= lauxlib.o lbaselib.o lcorolib.o ldblib.o liolib.o lmathlib.o loadlib.o loslib.o lstrlib.o ltablib.o lutf8lib.o linit.o BASE_O= $(CORE_O) $(LIB_O) $(MYOBJS) - - -diff -ruN lua-5.4.3/src/utf8_wrappers.c newlua/src/utf8_wrappers.c ---- lua-5.4.3/src/utf8_wrappers.c 1970-01-01 07:30:00.000000000 +0730 -+++ newlua/src/utf8_wrappers.c 2022-06-10 19:13:11.904613300 +0800 + +diff -ruN lua-5.4.4/src/utf8_wrappers.c lua-5.4.4-mod/src/utf8_wrappers.c +--- lua-5.4.4/src/utf8_wrappers.c 1970-01-01 07:30:00.000000000 +0730 ++++ lua-5.4.4-mod/src/utf8_wrappers.c 2022-11-16 10:09:04.583866600 +0800 @@ -0,0 +1,101 @@ +/** + * Wrappers to provide Unicode (UTF-8) support on Windows. @@ -147,10 +145,10 @@ diff -ruN lua-5.4.3/src/utf8_wrappers.c newlua/src/utf8_wrappers.c + return LoadLibraryExW(pathname_w, hFile, dwFlags); +} +#endif -diff -ruN lua-5.4.3/src/utf8_wrappers.h newlua/src/utf8_wrappers.h ---- lua-5.4.3/src/utf8_wrappers.h 1970-01-01 07:30:00.000000000 +0730 -+++ newlua/src/utf8_wrappers.h 2022-06-10 19:22:53.554879400 +0800 -@@ -0,0 +1,42 @@ +diff -ruN lua-5.4.4/src/utf8_wrappers.h lua-5.4.4-mod/src/utf8_wrappers.h +--- lua-5.4.4/src/utf8_wrappers.h 1970-01-01 07:30:00.000000000 +0730 ++++ lua-5.4.4-mod/src/utf8_wrappers.h 2022-11-16 10:29:46.044102000 +0800 +@@ -0,0 +1,44 @@ +/** + * Wrappers to provide Unicode (UTF-8) support on Windows. + * @@ -167,6 +165,7 @@ diff -ruN lua-5.4.3/src/utf8_wrappers.h newlua/src/utf8_wrappers.h +#endif + +#ifdef lauxlib_c ++#include +FILE *freopen_utf8(const char *pathname, const char *mode, FILE *stream); +#define freopen freopen_utf8 +#endif @@ -177,6 +176,7 @@ diff -ruN lua-5.4.3/src/utf8_wrappers.h newlua/src/utf8_wrappers.h +#endif + +#ifdef loslib_c ++#include +int remove_utf8(const char *pathname); +int rename_utf8(const char *oldpath, const char *newpath); +int system_utf8(const char *command); diff --git a/scripts/build.sh b/scripts/build.sh index 9f8e6567..5812bd8b 100644 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -31,6 +31,7 @@ show_help() { main() { local platform="$(get_platform_name)" + local arch="$(get_platform_arch)" local build_dir="$(get_default_build_dir)" local build_type="debug" local prefix=/ @@ -106,11 +107,27 @@ main() { portable="" fi + if [[ $CROSS_ARCH != "" ]]; then + if [[ $platform == "macos" ]]; then + macos_version_min=10.11 + if [[ $CROSS_ARCH == "arm64" ]]; then + cross_file="--cross-file resources/macos/macos_arm64.conf" + macos_version_min=11.0 + fi + export MACOSX_DEPLOYMENT_TARGET=$macos_version_min + export MIN_SUPPORTED_MACOSX_DEPLOYMENT_TARGET=$macos_version_min + export CFLAGS=-mmacosx-version-min=$macos_version_min + export CXXFLAGS=-mmacosx-version-min=$macos_version_min + export LDFLAGS=-mmacosx-version-min=$macos_version_min + fi + fi + rm -rf "${build_dir}" CFLAGS=$CFLAGS LDFLAGS=$LDFLAGS meson setup \ --buildtype=$build_type \ --prefix "$prefix" \ + $cross_file \ $force_fallback \ $bundle \ $portable \ @@ -124,7 +141,7 @@ main() { meson compile -C "${build_dir}" - if [ ! -z ${pgo+x} ]; then + if [[ $pgo != "" ]]; then cp -r data "${build_dir}/src" "${build_dir}/src/lite-xl" meson configure -Db_pgo=use "${build_dir}" diff --git a/scripts/common.sh b/scripts/common.sh index f598e45c..7078c99c 100644 --- a/scripts/common.sh +++ b/scripts/common.sh @@ -27,16 +27,17 @@ addons_download() { -o "${build_dir}/lite-xl-widgets.zip" unzip "${build_dir}/lite-xl-widgets.zip" -d "${build_dir}" - mv "${build_dir}/lite-xl-widgets-master" "${build_dir}/third/data/widget" + mkdir -p "${build_dir}/third/data/libraries" + mv "${build_dir}/lite-xl-widgets-master" "${build_dir}/third/data/libraries/widget" # Downlaod thirdparty plugins curl --insecure \ - -L "https://github.com/lite-xl/lite-xl-plugins/archive/2.1.zip" \ + -L "https://github.com/lite-xl/lite-xl-plugins/archive/master.zip" \ -o "${build_dir}/lite-xl-plugins.zip" unzip "${build_dir}/lite-xl-plugins.zip" -d "${build_dir}" - mv "${build_dir}/lite-xl-plugins-2.1/plugins" "${build_dir}/third/data" - rm -rf "${build_dir}/lite-xl-plugins-2.1" + mv "${build_dir}/lite-xl-plugins-master/plugins" "${build_dir}/third/data" + rm -rf "${build_dir}/lite-xl-plugins-master" } # Addons installation: some distributions forbid external downloads @@ -45,7 +46,7 @@ addons_install() { local build_dir="$1" local data_dir="$2" - for module_name in colors widget; do + for module_name in colors libraries; do cp -r "${build_dir}/third/data/$module_name" "${data_dir}" done @@ -81,6 +82,8 @@ get_platform_arch() { else arch=i686 fi + elif [[ $CROSS_ARCH != "" ]]; then + arch=$CROSS_ARCH fi echo "$arch" } diff --git a/scripts/fontello-config.json b/scripts/fontello-config.json index 42334c2f..76349182 100644 --- a/scripts/fontello-config.json +++ b/scripts/fontello-config.json @@ -125,6 +125,76 @@ "css": "right-open", "code": 62, "src": "fontawesome" + }, + { + "uid": "ebf4bfe82c54f9beb94c5221a7bdd975", + "css": "lite-xlbg", + "code": 53, + "src": "custom_icons", + "selected": true, + "svg": { + "path": "M83.3 0H916.7C962.7 0 1000 37.3 1000 83.3V916.7C1000 962.7 962.7 1000 916.7 1000H83.3C37.3 1000 0 962.7 0 916.7V83.3C0 37.3 37.3 0 83.3 0Z", + "width": 1000 + }, + "search": [ + "lite-xlbg" + ] + }, + { + "uid": "cde50fad6c2cd7a805a8d5026939d645", + "css": "lite-xl1", + "code": 54, + "src": "custom_icons", + "selected": true, + "svg": { + "path": "M395.8 562.5V312.5C395.8 289.5 377.2 270.8 354.2 270.8H270.8V625C270.8 682.5 317.5 729.2 375 729.2H729.2V645.8C729.2 622.8 710.5 604.2 687.5 604.2H437.5C414.5 604.2 395.8 585.5 395.8 562.5Z", + "width": 1000 + }, + "search": [ + "lite-xl1" + ] + }, + { + "uid": "526539ec7fcda0b3e7e7ceaa6ae416e6", + "css": "lite-xl2", + "code": 55, + "src": "custom_icons", + "selected": true, + "svg": { + "path": "M729.2 270.8H437.5L729.2 562.5Z", + "width": 1000 + }, + "search": [ + "lite-xl2" + ] + }, + { + "uid": "cccbe047c5cb17c63c41a1fb1fc022f4", + "css": "lite-xl3", + "code": 57, + "src": "custom_icons", + "selected": true, + "svg": { + "path": "M666.7 333.3H500L666.7 500Z", + "width": 1000 + }, + "search": [ + "lite-xl3" + ] + }, + { + "uid": "09d781dca1f50fcae90e67df9cc2f8dd", + "css": "lite-xl4", + "code": 56, + "src": "custom_icons", + "selected": true, + "svg": { + "path": "M500 458.3V333.3L604.2 395.8 666.7 500H541.7C518.7 500 500 481.3 500 458.3Z", + "width": 1000 + }, + "search": [ + "lite-xl4" + ] } ] } \ No newline at end of file diff --git a/scripts/generate_header.sh b/scripts/generate_header.sh index 8d5da527..6db72f30 100755 --- a/scripts/generate_header.sh +++ b/scripts/generate_header.sh @@ -3,7 +3,7 @@ ##### CONFIG # symbols to ignore -IGNORE_SYM='luaL_pushmodule\|luaL_openlib' +IGNORE_SYM='luaL_pushmodule' ##### CONFIG @@ -76,7 +76,7 @@ generate_header() { echo "The lite_xl plugin API is quite simple. Any shared library can be a plugin file, so long" echo "as it has an entrypoint that looks like the following, where xxxxx is the plugin name:" echo '#include "lite_xl_plugin_api.h"' - echo "int lua_open_lite_xl_xxxxx(lua_State* L, void* XL) {" + echo "int luaopen_lite_xl_xxxxx(lua_State* L, void* XL) {" echo " lite_xl_plugin_init(XL);" echo " ..." echo " return 1;" @@ -98,6 +98,8 @@ generate_header() { decl "$LUA_PATH/lua.h" echo decl "$LUA_PATH/lauxlib.h" + echo "static void (*luaL_openlibs) (lua_State *L);" + echo 'static void __lite_xl_fallback_luaL_openlibs (lua_State *L) { fputs("warning: luaL_openlibs is a stub", stderr); }' echo echo "#define IMPORT_SYMBOL(name, ret, ...) name = (name = (ret (*) (__VA_ARGS__)) symbol(#name), name == NULL ? &__lite_xl_fallback_##name : name)" @@ -106,6 +108,7 @@ generate_header() { decl_import "$LUA_PATH/lua.h" decl_import "$LUA_PATH/lauxlib.h" + echo -e "\tIMPORT_SYMBOL(luaL_openlibs, void, lua_State* L);" echo "}" echo "#endif" diff --git a/src/api/api.h b/src/api/api.h index c11fbdb3..e27112c6 100644 --- a/src/api/api.h +++ b/src/api/api.h @@ -8,6 +8,7 @@ #define API_TYPE_FONT "Font" #define API_TYPE_PROCESS "Process" #define API_TYPE_DIRMONITOR "Dirmonitor" +#define API_TYPE_NATIVE_PLUGIN "NativePlugin" #define API_CONSTANT_DEFINE(L, idx, key, n) (lua_pushnumber(L, n), lua_setfield(L, idx - 1, key)) diff --git a/src/api/dirmonitor.c b/src/api/dirmonitor.c index 73dfb348..1bccfd13 100644 --- a/src/api/dirmonitor.c +++ b/src/api/dirmonitor.c @@ -1,8 +1,6 @@ #include "api.h" #include #include -#include -#include #include #include @@ -23,6 +21,7 @@ int get_changes_dirmonitor(struct dirmonitor_internal*, char*, int); int translate_changes_dirmonitor(struct dirmonitor_internal*, char*, int, int (*)(int, const char*, void*), void*); int add_dirmonitor(struct dirmonitor_internal*, const char*); void remove_dirmonitor(struct dirmonitor_internal*, int); +int get_mode_dirmonitor(); static int f_check_dir_callback(int watch_id, const char* path, void* L) { @@ -62,6 +61,7 @@ static int f_dirmonitor_new(lua_State* L) { struct dirmonitor* monitor = lua_newuserdata(L, sizeof(struct dirmonitor)); luaL_setmetatable(L, API_TYPE_DIRMONITOR); memset(monitor, 0, sizeof(struct dirmonitor)); + monitor->mutex = SDL_CreateMutex(); monitor->internal = init_dirmonitor(); return 1; } @@ -111,12 +111,23 @@ static int f_dirmonitor_check(lua_State* L) { } +static int f_dirmonitor_mode(lua_State* L) { + int mode = get_mode_dirmonitor(); + if (mode == 1) + lua_pushstring(L, "single"); + else + lua_pushstring(L, "multiple"); + return 1; +} + + static const luaL_Reg dirmonitor_lib[] = { { "new", f_dirmonitor_new }, { "__gc", f_dirmonitor_gc }, { "watch", f_dirmonitor_watch }, { "unwatch", f_dirmonitor_unwatch }, { "check", f_dirmonitor_check }, + { "mode", f_dirmonitor_mode }, {NULL, NULL} }; diff --git a/src/api/dirmonitor/dummy.c b/src/api/dirmonitor/dummy.c index 62b1e624..47aa0990 100644 --- a/src/api/dirmonitor/dummy.c +++ b/src/api/dirmonitor/dummy.c @@ -5,4 +5,5 @@ void deinit_dirmonitor(struct dirmonitor_internal* monitor) { } int get_changes_dirmonitor(struct dirmonitor_internal* monitor, char* buffer, size_t len) { return -1; } int translate_changes_dirmonitor(struct dirmonitor_internal* monitor, char* buffer, int size, int (*callback)(int, const char*, void*), void* data) { return -1; } int add_dirmonitor(struct dirmonitor_internal* monitor, const char* path) { return -1; } -void remove_dirmonitor(struct dirmonitor_internal* monitor, int fd) { } \ No newline at end of file +void remove_dirmonitor(struct dirmonitor_internal* monitor, int fd) { } +int get_mode_dirmonitor() { return 1; } diff --git a/src/api/dirmonitor/fsevents.c b/src/api/dirmonitor/fsevents.c new file mode 100644 index 00000000..47812f49 --- /dev/null +++ b/src/api/dirmonitor/fsevents.c @@ -0,0 +1,193 @@ +#include +#include + +struct dirmonitor_internal { + SDL_mutex* lock; + char** changes; + size_t count; + FSEventStreamRef stream; + int fds[2]; +}; + +CFRunLoopRef main_run_loop; + + +struct dirmonitor_internal* init_dirmonitor() { + static bool mainloop_registered = false; + if (!mainloop_registered) { + main_run_loop = CFRunLoopGetCurrent(); + mainloop_registered = true; + } + + struct dirmonitor_internal* monitor = malloc(sizeof(struct dirmonitor_internal)); + monitor->stream = NULL; + monitor->changes = NULL; + monitor->count = 0; + monitor->lock = NULL; + + return monitor; +} + + +static void stop_monitor_stream(struct dirmonitor_internal* monitor) { + if (monitor->stream) { + FSEventStreamStop(monitor->stream); + FSEventStreamUnscheduleFromRunLoop( + monitor->stream, main_run_loop, kCFRunLoopDefaultMode + ); + FSEventStreamInvalidate(monitor->stream); + FSEventStreamRelease(monitor->stream); + monitor->stream = NULL; + + SDL_LockMutex(monitor->lock); + write(monitor->fds[1], "", 1); + close(monitor->fds[0]); + close(monitor->fds[1]); + if (monitor->count > 0) { + for (size_t i = 0; icount; i++) { + free(monitor->changes[i]); + } + free(monitor->changes); + monitor->changes = NULL; + monitor->count = 0; + } + SDL_UnlockMutex(monitor->lock); + SDL_DestroyMutex(monitor->lock); + } +} + + +void deinit_dirmonitor(struct dirmonitor_internal* monitor) { + stop_monitor_stream(monitor); +} + + +static void stream_callback( + ConstFSEventStreamRef streamRef, + void* monitor_ptr, + size_t numEvents, + void* eventPaths, + const FSEventStreamEventFlags eventFlags[], + const FSEventStreamEventId eventIds[] +) +{ + if (numEvents <= 0) { + return; + } + + struct dirmonitor_internal* monitor = monitor_ptr; + char** path_list = eventPaths; + + SDL_LockMutex(monitor->lock); + size_t total = 0; + if (monitor->count == 0) { + total = numEvents; + monitor->changes = calloc(numEvents, sizeof(char*)); + } else { + total = monitor->count + numEvents; + monitor->changes = realloc( + monitor->changes, + sizeof(char*) * total + ); + } + for (size_t idx = monitor->count; idx < total; idx++) { + size_t pidx = idx - monitor->count; + monitor->changes[idx] = malloc(strlen(path_list[pidx])+1); + strcpy(monitor->changes[idx], path_list[pidx]); + } + monitor->count = total; + + if (total > 0) + write(monitor->fds[1], "", 1); + SDL_UnlockMutex(monitor->lock); +} + + +int get_changes_dirmonitor( + struct dirmonitor_internal* monitor, + char* buffer, + int buffer_size +) { + char response[1]; + read(monitor->fds[0], response, 1); + + size_t results = 0; + SDL_LockMutex(monitor->lock); + results = monitor->count; + SDL_UnlockMutex(monitor->lock); + + return results; +} + + +int translate_changes_dirmonitor( + struct dirmonitor_internal* monitor, + char* buffer, + int buffer_size, + int (*change_callback)(int, const char*, void*), + void* L +) { + SDL_LockMutex(monitor->lock); + if (monitor->count > 0) { + for (size_t i = 0; icount; i++) { + change_callback(strlen(monitor->changes[i]), monitor->changes[i], L); + free(monitor->changes[i]); + } + free(monitor->changes); + monitor->changes = NULL; + monitor->count = 0; + } + SDL_UnlockMutex(monitor->lock); + return 0; +} + + +int add_dirmonitor(struct dirmonitor_internal* monitor, const char* path) { + stop_monitor_stream(monitor); + + monitor->lock = SDL_CreateMutex(); + pipe(monitor->fds); + + FSEventStreamContext context = { + .info = monitor, + .retain = NULL, + .release = NULL, + .copyDescription = NULL, + .version = 0 + }; + + CFStringRef paths[] = { + CFStringCreateWithCString(NULL, path, kCFStringEncodingUTF8) + }; + + monitor->stream = FSEventStreamCreate( + NULL, + stream_callback, + &context, + CFArrayCreate(NULL, (const void **)&paths, 1, NULL), + kFSEventStreamEventIdSinceNow, + 0, + kFSEventStreamCreateFlagNone + | kFSEventStreamCreateFlagWatchRoot + | kFSEventStreamCreateFlagFileEvents + ); + + FSEventStreamScheduleWithRunLoop( + monitor->stream, main_run_loop, kCFRunLoopDefaultMode + ); + + if (!FSEventStreamStart(monitor->stream)) { + stop_monitor_stream(monitor); + return -1; + } + + return 1; +} + + +void remove_dirmonitor(struct dirmonitor_internal* monitor, int fd) { + stop_monitor_stream(monitor); +} + + +int get_mode_dirmonitor() { return 1; } diff --git a/src/api/dirmonitor/inotify.c b/src/api/dirmonitor/inotify.c index 697e1815..4d2f8458 100644 --- a/src/api/dirmonitor/inotify.c +++ b/src/api/dirmonitor/inotify.c @@ -13,7 +13,7 @@ struct dirmonitor_internal { struct dirmonitor_internal* init_dirmonitor() { - struct dirmonitor_internal* monitor = calloc(sizeof(struct dirmonitor_internal), 1); + struct dirmonitor_internal* monitor = calloc(1, sizeof(struct dirmonitor_internal)); monitor->fd = inotify_init(); pipe(monitor->sig); fcntl(monitor->sig[0], F_SETFD, FD_CLOEXEC); @@ -51,3 +51,6 @@ int add_dirmonitor(struct dirmonitor_internal* monitor, const char* path) { void remove_dirmonitor(struct dirmonitor_internal* monitor, int fd) { inotify_rm_watch(monitor->fd, fd); } + + +int get_mode_dirmonitor() { return 2; } diff --git a/src/api/dirmonitor/kqueue.c b/src/api/dirmonitor/kqueue.c index 7c6e89d8..69e3a74f 100644 --- a/src/api/dirmonitor/kqueue.c +++ b/src/api/dirmonitor/kqueue.c @@ -4,6 +4,7 @@ #include #include #include +#include struct dirmonitor_internal { int fd; @@ -11,7 +12,7 @@ struct dirmonitor_internal { struct dirmonitor_internal* init_dirmonitor() { - struct dirmonitor_internal* monitor = calloc(sizeof(struct dirmonitor_internal), 1); + struct dirmonitor_internal* monitor = calloc(1, sizeof(struct dirmonitor_internal)); monitor->fd = kqueue(); return monitor; } @@ -23,7 +24,9 @@ void deinit_dirmonitor(struct dirmonitor_internal* monitor) { int get_changes_dirmonitor(struct dirmonitor_internal* monitor, char* buffer, int buffer_size) { - int nev = kevent(monitor->fd, NULL, 0, (struct kevent*)buffer, buffer_size / sizeof(kevent), NULL); + struct timespec ts = { 0, 100 * 1000000 }; // 100 ms + + int nev = kevent(monitor->fd, NULL, 0, (struct kevent*)buffer, buffer_size / sizeof(kevent), &ts); if (nev == -1) return -1; if (nev <= 0) @@ -43,8 +46,11 @@ int add_dirmonitor(struct dirmonitor_internal* monitor, const char* path) { int fd = open(path, O_RDONLY); struct kevent change; + // a timeout of zero should make kevent return immediately + struct timespec ts = { 0, 0 }; // 0 s + EV_SET(&change, fd, EVFILT_VNODE, EV_ADD | EV_CLEAR, NOTE_DELETE | NOTE_EXTEND | NOTE_WRITE | NOTE_ATTRIB | NOTE_LINK | NOTE_RENAME, 0, (void*)path); - kevent(monitor->fd, &change, 1, NULL, 0, NULL); + kevent(monitor->fd, &change, 1, NULL, 0, &ts); return fd; } @@ -53,3 +59,6 @@ int add_dirmonitor(struct dirmonitor_internal* monitor, const char* path) { void remove_dirmonitor(struct dirmonitor_internal* monitor, int fd) { close(fd); } + + +int get_mode_dirmonitor() { return 2; } diff --git a/src/api/dirmonitor/win32.c b/src/api/dirmonitor/win32.c index 5483584f..c320b86d 100644 --- a/src/api/dirmonitor/win32.c +++ b/src/api/dirmonitor/win32.c @@ -19,7 +19,7 @@ int get_changes_dirmonitor(struct dirmonitor_internal* monitor, char* buffer, in struct dirmonitor* init_dirmonitor() { - return calloc(sizeof(struct dirmonitor_internal), 1); + return calloc(1, sizeof(struct dirmonitor_internal)); } @@ -40,8 +40,8 @@ void deinit_dirmonitor(struct dirmonitor_internal* monitor) { int translate_changes_dirmonitor(struct dirmonitor_internal* monitor, char* buffer, int buffer_size, int (*change_callback)(int, const char*, void*), void* data) { for (FILE_NOTIFY_INFORMATION* info = (FILE_NOTIFY_INFORMATION*)buffer; (char*)info < buffer + buffer_size; info = (FILE_NOTIFY_INFORMATION*)(((char*)info) + info->NextEntryOffset)) { - char transform_buffer[PATH_MAX*4]; - int count = WideCharToMultiByte(CP_UTF8, 0, (WCHAR*)info->FileName, info->FileNameLength, transform_buffer, PATH_MAX*4 - 1, NULL, NULL); + char transform_buffer[MAX_PATH*4]; + int count = WideCharToMultiByte(CP_UTF8, 0, (WCHAR*)info->FileName, info->FileNameLength / 2, transform_buffer, MAX_PATH*4 - 1, NULL, NULL); change_callback(count, transform_buffer, data); if (!info->NextEntryOffset) break; @@ -60,3 +60,6 @@ int add_dirmonitor(struct dirmonitor_internal* monitor, const char* path) { void remove_dirmonitor(struct dirmonitor_internal* monitor, int fd) { close_monitor_handle(monitor); } + + +int get_mode_dirmonitor() { return 1; } diff --git a/src/api/process.c b/src/api/process.c index d713a84b..60efe5f9 100644 --- a/src/api/process.c +++ b/src/api/process.c @@ -29,7 +29,7 @@ typedef int process_handle; #endif typedef struct { - bool running; + bool running, detached; int returncode, deadline; long pid; #if _WIN32 @@ -38,7 +38,7 @@ typedef struct { bool reading[2]; char buffer[2][READ_BUF_SIZE]; #endif - process_handle child_pipes[3][2]; + process_handle child_pipes[3][2]; } process_t; typedef enum { @@ -65,7 +65,7 @@ typedef enum { #ifdef _WIN32 static volatile long PipeSerialNumber; - static void close_fd(HANDLE* handle) { if (*handle) CloseHandle(*handle); *handle = NULL; } + static void close_fd(HANDLE* handle) { if (*handle) CloseHandle(*handle); *handle = INVALID_HANDLE_VALUE; } #else static void close_fd(int* fd) { if (*fd) close(*fd); *fd = 0; } #endif @@ -125,22 +125,26 @@ 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; + bool detach = false, literal = false; int deadline = 10, new_fds[3] = { STDIN_FD, STDOUT_FD, STDERR_FD }; - luaL_checktype(L, 1, LUA_TTABLE); - #if LUA_VERSION_NUM > 501 - lua_len(L, 1); - #else - lua_pushinteger(L, (int)lua_objlen(L, 1)); - #endif - size_t cmd_len = luaL_checknumber(L, -1); lua_pop(L, 1); - size_t arg_len = lua_gettop(L); - for (size_t i = 1; i <= cmd_len; ++i) { - lua_pushinteger(L, i); - lua_rawget(L, 1); - cmd[i-1] = luaL_checkstring(L, -1); + size_t arg_len = lua_gettop(L), cmd_len; + if (lua_type(L, 1) == LUA_TTABLE) { + #if LUA_VERSION_NUM > 501 + lua_len(L, 1); + #else + lua_pushinteger(L, (int)lua_objlen(L, 1)); + #endif + cmd_len = luaL_checknumber(L, -1); lua_pop(L, 1); + 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; } - // this should never trip // but if it does we are in deep trouble assert(cmd[0]); @@ -169,11 +173,8 @@ static int process_start(lua_State* L) { 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) { - for (size_t i = 0; i < env_len; ++i) { - free((char*)env_names[i]); - free((char*)env_values[i]); - } - retval = luaL_error(L, "redirect to handles, FILE* and paths are not supported"); + lua_pushfstring(L, "redirect to handles, FILE* and paths are not supported"); + retval = -1; goto cleanup; } } @@ -183,6 +184,7 @@ static int process_start(lua_State* L) { memset(self, 0, sizeof(process_t)); luaL_setmetatable(L, API_TYPE_PROCESS); self->deadline = deadline; + self->detached = detach; #if _WIN32 for (int i = 0; i < 3; ++i) { switch (new_fds[i]) { @@ -205,18 +207,21 @@ static int process_start(lua_State* L) { self->child_pipes[i][0] = CreateNamedPipeA(pipeNameBuffer, PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED, PIPE_TYPE_BYTE | PIPE_WAIT, 1, READ_BUF_SIZE, READ_BUF_SIZE, 0, NULL); if (self->child_pipes[i][0] == INVALID_HANDLE_VALUE) { - retval = luaL_error(L, "Error creating read pipe: %d.", GetLastError()); + lua_pushfstring(L, "Error creating read pipe: %d.", GetLastError()); + retval = -1; goto cleanup; } self->child_pipes[i][1] = CreateFileA(pipeNameBuffer, GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (self->child_pipes[i][1] == INVALID_HANDLE_VALUE) { CloseHandle(self->child_pipes[i][0]); - retval = luaL_error(L, "Error creating write pipe: %d.", GetLastError()); + lua_pushfstring(L, "Error creating write pipe: %d.", GetLastError()); + retval = -1; goto cleanup; } if (!SetHandleInformation(self->child_pipes[i][i == STDIN_FD ? 1 : 0], HANDLE_FLAG_INHERIT, 0) || !SetHandleInformation(self->child_pipes[i][i == STDIN_FD ? 0 : 1], HANDLE_FLAG_INHERIT, 1)) { - retval = luaL_error(L, "Error inheriting pipes: %d.", GetLastError()); + lua_pushfstring(L, "Error inheriting pipes: %d.", GetLastError()); + retval = -1; goto cleanup; } } @@ -238,15 +243,35 @@ static int process_start(lua_State* L) { 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]; - strcpy(commandLine, cmd[0]); int offset = 0; - for (size_t i = 1; i < cmd_len; ++i) { - size_t len = strlen(cmd[i]); - offset += len + 1; - if (offset >= sizeof(commandLine)) - break; - strcat(commandLine, " "); - strcat(commandLine, cmd[i]); + 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) { @@ -259,7 +284,8 @@ static int process_start(lua_State* L) { 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)) { - retval = luaL_error(L, "Error creating a process: %d.", GetLastError()); + lua_pushfstring(L, "Error creating a process: %d.", GetLastError()); + retval = -1; goto cleanup; } self->pid = (long)self->process_information.dwProcessId; @@ -269,16 +295,19 @@ static int process_start(lua_State* L) { #else for (int i = 0; i < 3; ++i) { // Make only the parents fd's non-blocking. Children should block. if (pipe(self->child_pipes[i]) || fcntl(self->child_pipes[i][i == STDIN_FD ? 1 : 0], F_SETFL, O_NONBLOCK) == -1) { - retval = luaL_error(L, "Error creating pipes: %s", strerror(errno)); + lua_pushfstring(L, "Error creating pipes: %s", strerror(errno)); + retval = -1; goto cleanup; } } self->pid = (long)fork(); if (self->pid < 0) { - retval = luaL_error(L, "Error running fork: %s.", strerror(errno)); + lua_pushfstring(L, "Error running fork: %s.", strerror(errno)); + retval = -1; goto cleanup; } else if (!self->pid) { - setpgid(0,0); + if (!detach) + setpgid(0,0); for (int stream = 0; stream < 3; ++stream) { if (new_fds[stream] == REDIRECT_DISCARD) { // Close the stream if we don't want it. close(self->child_pipes[stream][stream == STDIN_FD ? 0 : 1]); @@ -307,6 +336,9 @@ static int process_start(lua_State* L) { close_fd(pipe); } } + if (retval == -1) + return lua_error(L); + self->running = true; return retval; } @@ -454,7 +486,8 @@ static int f_kill(lua_State* L) { return self_signal(L, SIGNAL_KILL); } static int f_interrupt(lua_State* L) { return self_signal(L, SIGNAL_INTERRUPT); } static int f_gc(lua_State* L) { process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); - signal_process(self, SIGNAL_TERM); + if (!self->detached) + signal_process(self, SIGNAL_TERM); close_fd(&self->child_pipes[STDIN_FD ][1]); close_fd(&self->child_pipes[STDOUT_FD][0]); close_fd(&self->child_pipes[STDERR_FD][0]); diff --git a/src/api/regex.c b/src/api/regex.c index d23eaf71..f86e7ff6 100644 --- a/src/api/regex.c +++ b/src/api/regex.c @@ -4,6 +4,126 @@ #include #include +#include + +typedef struct RegexState { + pcre2_code* re; + pcre2_match_data* match_data; + const char* subject; + size_t subject_len; + size_t offset; + bool regex_compiled; + bool found; +} RegexState; + +static pcre2_code* regex_get_pattern(lua_State *L, bool* should_free) { + pcre2_code* re = NULL; + *should_free = false; + + if (lua_type(L, 1) == LUA_TTABLE) { + lua_rawgeti(L, 1, 1); + re = (pcre2_code*)lua_touserdata(L, -1); + lua_settop(L, -2); + } else { + int errornumber; + PCRE2_SIZE erroroffset; + size_t pattern_len = 0; + const char* pattern = luaL_checklstring(L, 1, &pattern_len); + + re = pcre2_compile( + (PCRE2_SPTR)pattern, + pattern_len, PCRE2_UTF, + &errornumber, &erroroffset, NULL + ); + + if (re == NULL) { + PCRE2_UCHAR errmsg[256]; + pcre2_get_error_message(errornumber, errmsg, sizeof(errmsg)); + luaL_error( + L, "regex pattern error at offset %d: %s", + (int)erroroffset, errmsg + ); + return NULL; + } + + pcre2_jit_compile(re, PCRE2_JIT_COMPLETE); + + *should_free = true; + } + + return re; +} + +static int regex_gmatch_iterator(lua_State *L) { + RegexState *state = (RegexState*)lua_touserdata(L, lua_upvalueindex(3)); + + if (state->found) { + int rc = pcre2_match( + state->re, + (PCRE2_SPTR)state->subject, state->subject_len, + state->offset, 0, state->match_data, NULL + ); + + if (rc < 0) { + if (rc != PCRE2_ERROR_NOMATCH) { + PCRE2_UCHAR buffer[120]; + pcre2_get_error_message(rc, buffer, sizeof(buffer)); + luaL_error(L, "regex matching error %d: %s", rc, buffer); + } + goto clean; + } else { + size_t ovector_count = pcre2_get_ovector_count(state->match_data); + if (ovector_count > 0) { + PCRE2_SIZE* ovector = pcre2_get_ovector_pointer(state->match_data); + if (ovector[0] > ovector[1]) { + /* We must guard against patterns such as /(?=.\K)/ that use \K in an + assertion to set the start of a match later than its end. In the editor, + we just detect this case and give up. */ + luaL_error(L, "regex matching error: \\K was used in an assertion to " + " set the match start after its end"); + goto clean; + } + + int index = 0; + if (ovector_count > 1) index = 2; + + int total = 0; + int total_results = ovector_count * 2; + size_t last_offset = 0; + for (int i = index; i < total_results; i+=2) { + lua_pushlstring(L, state->subject+ovector[i], ovector[i+1] - ovector[i]); + last_offset = ovector[i+1]; + total++; + } + + if (last_offset - 1 < state->subject_len) + state->offset = last_offset; + else + state->found = false; + + return total; + } else { + state->found = false; + } + } + } + +clean: + if (state->regex_compiled) pcre2_code_free(state->re); + pcre2_match_data_free(state->match_data); + + return 0; /* not found */ +} + +static size_t regex_offset_relative(lua_Integer pos, size_t len) { + if (pos > 0) + return (size_t)pos; + else if (pos == 0) + return 1; + else if (pos < -(lua_Integer)len) /* inverted comparison */ + return 1; /* clip to 1 */ + else return len + (size_t)pos + 1; +} static int f_pcre_gc(lua_State* L) { lua_rawgeti(L, -1, 1); @@ -37,6 +157,7 @@ static int f_pcre_compile(lua_State *L) { NULL ); if (re) { + pcre2_jit_compile(re, PCRE2_JIT_COMPLETE); lua_newtable(L); lua_pushlightuserdata(L, re); lua_rawseti(L, -2, 1); @@ -56,19 +177,21 @@ static int f_pcre_compile(lua_State *L) { // (including the whole match), if a match was found. static int f_pcre_match(lua_State *L) { size_t len, offset = 1, opts = 0; - luaL_checktype(L, 1, LUA_TTABLE); + bool regex_compiled = false; + pcre2_code* re = regex_get_pattern(L, ®ex_compiled); + if (!re) return 0 ; const char* str = luaL_checklstring(L, 2, &len); if (lua_gettop(L) > 2) - offset = luaL_checknumber(L, 3); + offset = regex_offset_relative(luaL_checknumber(L, 3), len); offset -= 1; len -= offset; if (lua_gettop(L) > 3) opts = luaL_checknumber(L, 4); lua_rawgeti(L, 1, 1); - pcre2_code* re = (pcre2_code*)lua_touserdata(L, -1); pcre2_match_data* md = pcre2_match_data_create_from_pattern(re, NULL); int rc = pcre2_match(re, (PCRE2_SPTR)&str[offset], len, 0, opts, md, NULL); if (rc < 0) { + if (regex_compiled) pcre2_code_free(re); pcre2_match_data_free(md); if (rc != PCRE2_ERROR_NOMATCH) { PCRE2_UCHAR buffer[120]; @@ -84,18 +207,155 @@ static int f_pcre_match(lua_State *L) { we just detect this case and give up. */ luaL_error(L, "regex matching error: \\K was used in an assertion to " " set the match start after its end"); + if (regex_compiled) pcre2_code_free(re); pcre2_match_data_free(md); return 0; } for (int i = 0; i < rc*2; i++) lua_pushinteger(L, ovector[i]+offset+1); + if (regex_compiled) pcre2_code_free(re); pcre2_match_data_free(md); return rc*2; } +static int f_pcre_gmatch(lua_State *L) { + /* pattern param */ + bool regex_compiled = false; + pcre2_code* re = regex_get_pattern(L, ®ex_compiled); + if (!re) return 0; + size_t subject_len = 0; + + /* subject param */ + const char* subject = luaL_checklstring(L, 2, &subject_len); + + /* offset param */ + size_t offset = regex_offset_relative( + luaL_optnumber(L, 3, 1), subject_len + ) - 1; + + /* keep strings on closure to avoid being collected */ + lua_settop(L, 2); + + RegexState *state; + state = (RegexState*)lua_newuserdata(L, sizeof(RegexState)); + + state->re = re; + state->match_data = pcre2_match_data_create_from_pattern(re, NULL); + state->subject = subject; + state->subject_len = subject_len; + state->offset = offset; + state->found = true; + state->regex_compiled = regex_compiled; + + lua_pushcclosure(L, regex_gmatch_iterator, 3); + return 1; +} + +static int f_pcre_gsub(lua_State *L) { + size_t subject_len = 0, replacement_len = 0; + + bool regex_compiled = false; + pcre2_code* re = regex_get_pattern(L, ®ex_compiled); + if (!re) return 0 ; + + char* subject = (char*) luaL_checklstring(L, 2, &subject_len); + const char* replacement = luaL_checklstring(L, 3, &replacement_len); + int limit = luaL_optinteger(L, 4, 0); + if (limit < 0 ) limit = 0; + + pcre2_match_data* match_data = pcre2_match_data_create_from_pattern(re, NULL); + + size_t buffer_size = 1024; + char *output = (char *)malloc(buffer_size); + + int options = PCRE2_SUBSTITUTE_OVERFLOW_LENGTH | PCRE2_SUBSTITUTE_EXTENDED; + if (limit == 0) options |= PCRE2_SUBSTITUTE_GLOBAL; + + int results_count = 0; + int limit_count = 0; + bool done = false; + size_t offset = 0; + PCRE2_SIZE outlen = buffer_size; + while (!done) { + results_count = pcre2_substitute( + re, + (PCRE2_SPTR)subject, subject_len, + offset, options, + match_data, NULL, + (PCRE2_SPTR)replacement, replacement_len, + (PCRE2_UCHAR*)output, &outlen + ); + + if (results_count != PCRE2_ERROR_NOMEMORY || buffer_size >= outlen) { + /* PCRE2_SUBSTITUTE_GLOBAL code path (fastest) */ + if(limit == 0) { + done = true; + /* non PCRE2_SUBSTITUTE_GLOBAL with limit code path (slower) */ + } else { + size_t ovector_count = pcre2_get_ovector_count(match_data); + if (results_count > 0 && ovector_count > 0) { + limit_count++; + PCRE2_SIZE* ovector = pcre2_get_ovector_pointer(match_data); + if (outlen > subject_len) { + offset = ovector[1] + (outlen - subject_len); + } else { + offset = ovector[1] - (subject_len - outlen); + } + if (limit_count > 1) free(subject); + if (limit_count == limit || offset-1 == outlen) { + done = true; + results_count = limit_count; + } else { + subject = output; + subject_len = outlen; + output = (char *)malloc(buffer_size); + outlen = buffer_size; + } + } else { + if (limit_count > 1) { + free(subject); + } + done = true; + results_count = limit_count; + } + } + } else { + buffer_size = outlen; + output = (char *)realloc(output, buffer_size); + } + } + + int return_count = 0; + + if (results_count > 0) { + lua_pushlstring(L, (const char*) output, outlen); + lua_pushinteger(L, results_count); + return_count = 2; + } else if (results_count == 0) { + lua_pushlstring(L, subject, subject_len); + lua_pushinteger(L, 0); + return_count = 2; + } + + free(output); + pcre2_match_data_free(match_data); + if (regex_compiled) + pcre2_code_free(re); + + if (results_count < 0) { + PCRE2_UCHAR errmsg[256]; + pcre2_get_error_message(results_count, errmsg, sizeof(errmsg)); + return luaL_error(L, "regex substitute error: %s", errmsg); + } + + return return_count; +} + static const luaL_Reg lib[] = { { "compile", f_pcre_compile }, { "cmatch", f_pcre_match }, + { "gmatch", f_pcre_gmatch }, + { "gsub", f_pcre_gsub }, { "__gc", f_pcre_gc }, { NULL, NULL } }; diff --git a/src/api/renderer.c b/src/api/renderer.c index d9ab83f6..77ff1ff6 100644 --- a/src/api/renderer.c +++ b/src/api/renderer.c @@ -1,7 +1,16 @@ #include #include "api.h" +<<<<<<< HEAD #include "renderer.h" #include "rencache.h" +======= +#include "../renderer.h" +#include "../rencache.h" +#include "lua.h" + +// a reference index to a table that stores the fonts +static int RENDERER_FONT_REF = LUA_NOREF; +>>>>>>> master static int font_get_options( lua_State *L, @@ -137,7 +146,22 @@ static int f_font_copy(lua_State *L) { } static int f_font_group(lua_State* L) { + int table_size; luaL_checktype(L, 1, LUA_TTABLE); + + table_size = lua_rawlen(L, 1); + if (table_size <= 0) + return luaL_error(L, "failed to create font group: table is empty"); + if (table_size > FONT_FALLBACK_MAX) + return luaL_error(L, "failed to create font group: table size too large"); + + // we also need to ensure that there are no fontgroups inside it + for (int i = 1; i <= table_size; i++) { + if (lua_rawgeti(L, 1, i) != LUA_TUSERDATA) + return luaL_typeerror(L, -1, API_TYPE_FONT "(userdata)"); + lua_pop(L, 1); + } + luaL_setmetatable(L, API_TYPE_FONT); return 1; } @@ -176,7 +200,10 @@ static int f_font_gc(lua_State *L) { static int f_font_get_width(lua_State *L) { RenFont* fonts[FONT_FALLBACK_MAX]; font_retrieve(L, fonts, 1); - lua_pushnumber(L, ren_font_group_get_width(fonts, luaL_checkstring(L, 2))); + size_t len; + const char *text = luaL_checklstring(L, 2, &len); + + lua_pushnumber(L, ren_font_group_get_width(fonts, text, len)); return 1; } @@ -199,19 +226,47 @@ static int f_font_set_size(lua_State *L) { return 0; } +static int color_value_error(lua_State *L, int idx, int table_idx) { + const char *type, *msg; + // generate an appropriate error message + if (luaL_getmetafield(L, -1, "__name") == LUA_TSTRING) { + type = lua_tostring(L, -1); // metatable name + } else if (lua_type(L, -1) == LUA_TLIGHTUSERDATA) { + type = "light userdata"; // special name for light userdata + } else { + type = lua_typename(L, lua_type(L, -1)); // default name + } + // the reason it went through so much hoops is to generate the correct error + // message (with function name and proper index). + msg = lua_pushfstring(L, "table[%d]: %s expected, got %s", table_idx, lua_typename(L, LUA_TNUMBER), type); + return luaL_argerror(L, idx, msg); +} + +static int get_color_value(lua_State *L, int idx, int table_idx) { + lua_rawgeti(L, idx, table_idx); + return lua_isnumber(L, -1) ? lua_tonumber(L, -1) : color_value_error(L, idx, table_idx); +} + +static int get_color_value_opt(lua_State *L, int idx, int table_idx, int default_value) { + lua_rawgeti(L, idx, table_idx); + if (lua_isnoneornil(L, -1)) + return default_value; + else if (lua_isnumber(L, -1)) + return lua_tonumber(L, -1); + else + return color_value_error(L, idx, table_idx); +} + static RenColor checkcolor(lua_State *L, int idx, int def) { RenColor color; if (lua_isnoneornil(L, idx)) { return (RenColor) { def, def, def, 255 }; } - lua_rawgeti(L, idx, 1); - lua_rawgeti(L, idx, 2); - lua_rawgeti(L, idx, 3); - lua_rawgeti(L, idx, 4); - color.r = luaL_checknumber(L, -4); - color.g = luaL_checknumber(L, -3); - color.b = luaL_checknumber(L, -2); - color.a = luaL_optnumber(L, -1, 255); + luaL_checktype(L, idx, LUA_TTABLE); + color.r = get_color_value(L, idx, 1); + color.g = get_color_value(L, idx, 2); + color.b = get_color_value(L, idx, 3); + color.a = get_color_value_opt(L, idx, 4, 255); lua_pop(L, 4); return color; } @@ -241,6 +296,9 @@ static int f_begin_frame(UNUSED lua_State *L) { static int f_end_frame(UNUSED lua_State *L) { rencache_end_frame(); + // clear the font reference table + lua_newtable(L); + lua_rawseti(L, LUA_REGISTRYINDEX, RENDERER_FONT_REF); return 0; } @@ -277,11 +335,25 @@ static int f_draw_rect(lua_State *L) { static int f_draw_text(lua_State *L) { RenFont* fonts[FONT_FALLBACK_MAX]; font_retrieve(L, fonts, 1); - const char *text = luaL_checkstring(L, 2); + + // stores a reference to this font to the reference table + lua_rawgeti(L, LUA_REGISTRYINDEX, RENDERER_FONT_REF); + if (lua_istable(L, -1)) + { + lua_pushvalue(L, 1); + lua_pushboolean(L, 1); + lua_rawset(L, -3); + } else { + fprintf(stderr, "warning: failed to reference count fonts\n"); + } + lua_pop(L, 1); + + size_t len; + const char *text = luaL_checklstring(L, 2, &len); float x = luaL_checknumber(L, 3); int y = luaL_checknumber(L, 4); RenColor color = checkcolor(L, 5, 255); - x = rencache_draw_text(fonts, text, x, y, color); + x = rencache_draw_text(fonts, text, len, x, y, color); lua_pushnumber(L, x); return 1; } @@ -312,6 +384,10 @@ static const luaL_Reg fontLib[] = { }; int luaopen_renderer(lua_State *L) { + // gets a reference on the registry to store font data + lua_newtable(L); + RENDERER_FONT_REF = luaL_ref(L, LUA_REGISTRYINDEX); + luaL_newlib(L, lib); luaL_newmetatable(L, API_TYPE_FONT); luaL_setfuncs(L, fontLib, 0); diff --git a/src/api/system.c b/src/api/system.c index 3d5364d9..6f4c9616 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -12,6 +12,20 @@ #include #include #include "../utfconv.h" + + // Windows does not define the S_ISREG and S_ISDIR macros in stat.h, so we do. + // We have to define _CRT_INTERNAL_NONSTDC_NAMES 1 before #including sys/stat.h + // in order for Microsoft's stat.h to define names like S_IFMT, S_IFREG, and S_IFDIR, + // rather than just defining _S_IFMT, _S_IFREG, and _S_IFDIR as it normally does. + #define _CRT_INTERNAL_NONSTDC_NAMES 1 + #include + #include + #if !defined(S_ISREG) && defined(S_IFMT) && defined(S_IFREG) + #define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) + #endif + #if !defined(S_ISDIR) && defined(S_IFMT) && defined(S_IFDIR) + #define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) + #endif #else #include @@ -169,8 +183,9 @@ static void push_win32_error(lua_State *L, DWORD rc) { static int f_poll_event(lua_State *L) { char buf[16]; - int mx, my, wx, wy; + int mx, my, w, h; SDL_Event e; + SDL_Event event_plus; top: if ( !SDL_PollEvent(&e) ) { @@ -220,12 +235,11 @@ top: goto top; case SDL_DROPFILE: - SDL_GetGlobalMouseState(&mx, &my); - SDL_GetWindowPosition(window, &wx, &wy); + SDL_GetMouseState(&mx, &my); lua_pushstring(L, "filedropped"); lua_pushstring(L, e.drop.file); - lua_pushinteger(L, mx - wx); - lua_pushinteger(L, my - wy); + lua_pushinteger(L, mx); + lua_pushinteger(L, my); SDL_free(e.drop.file); return 4; @@ -261,6 +275,23 @@ top: lua_pushstring(L, e.text.text); return 2; + case SDL_TEXTEDITING: + lua_pushstring(L, "textediting"); + lua_pushstring(L, e.edit.text); + lua_pushinteger(L, e.edit.start); + lua_pushinteger(L, e.edit.length); + return 4; + +#if SDL_VERSION_ATLEAST(2, 0, 22) + case SDL_TEXTEDITING_EXT: + lua_pushstring(L, "textediting"); + lua_pushstring(L, e.editExt.text); + lua_pushinteger(L, e.editExt.start); + lua_pushinteger(L, e.editExt.length); + SDL_free(e.editExt.text); + return 4; +#endif + case SDL_MOUSEBUTTONDOWN: if (e.button.button == 1) { SDL_CaptureMouse(1); } lua_pushstring(L, "mousepressed"); @@ -280,7 +311,6 @@ top: case SDL_MOUSEMOTION: SDL_PumpEvents(); - SDL_Event event_plus; while (SDL_PeepEvents(&event_plus, 1, SDL_GETEVENT, SDL_MOUSEMOTION, SDL_MOUSEMOTION) > 0) { e.motion.x = event_plus.motion.x; e.motion.y = event_plus.motion.y; @@ -296,8 +326,51 @@ top: case SDL_MOUSEWHEEL: lua_pushstring(L, "mousewheel"); +#if SDL_VERSION_ATLEAST(2, 0, 18) + lua_pushnumber(L, e.wheel.preciseY); + // Use -x to keep consistency with vertical scrolling values (e.g. shift+scroll) + lua_pushnumber(L, -e.wheel.preciseX); +#else lua_pushinteger(L, e.wheel.y); - return 2; + lua_pushinteger(L, -e.wheel.x); +#endif + return 3; + + case SDL_FINGERDOWN: + SDL_GetWindowSize(window, &w, &h); + + lua_pushstring(L, "touchpressed"); + lua_pushinteger(L, (lua_Integer)(e.tfinger.x * w)); + lua_pushinteger(L, (lua_Integer)(e.tfinger.y * h)); + lua_pushinteger(L, e.tfinger.fingerId); + return 4; + + case SDL_FINGERUP: + SDL_GetWindowSize(window, &w, &h); + + lua_pushstring(L, "touchreleased"); + lua_pushinteger(L, (lua_Integer)(e.tfinger.x * w)); + lua_pushinteger(L, (lua_Integer)(e.tfinger.y * h)); + lua_pushinteger(L, e.tfinger.fingerId); + return 4; + + case SDL_FINGERMOTION: + SDL_PumpEvents(); + while (SDL_PeepEvents(&event_plus, 1, SDL_GETEVENT, SDL_FINGERMOTION, SDL_FINGERMOTION) > 0) { + e.tfinger.x = event_plus.tfinger.x; + e.tfinger.y = event_plus.tfinger.y; + e.tfinger.dx += event_plus.tfinger.dx; + e.tfinger.dy += event_plus.tfinger.dy; + } + SDL_GetWindowSize(window, &w, &h); + + lua_pushstring(L, "touchmoved"); + lua_pushinteger(L, (lua_Integer)(e.tfinger.x * w)); + lua_pushinteger(L, (lua_Integer)(e.tfinger.y * h)); + lua_pushinteger(L, (lua_Integer)(e.tfinger.dx * w)); + lua_pushinteger(L, (lua_Integer)(e.tfinger.dy * h)); + lua_pushinteger(L, e.tfinger.fingerId); + return 6; default: goto top; @@ -441,6 +514,36 @@ static int f_get_window_mode(lua_State *L) { return 1; } +static int f_set_text_input_rect(lua_State *L) { + SDL_Rect rect; + rect.x = luaL_checknumber(L, 1); + rect.y = luaL_checknumber(L, 2); + rect.w = luaL_checknumber(L, 3); + rect.h = luaL_checknumber(L, 4); + SDL_SetTextInputRect(&rect); + return 0; +} + +static int f_clear_ime(lua_State *L) { +#if SDL_VERSION_ATLEAST(2, 0, 22) + SDL_ClearComposition(); +#endif + return 0; +} + + +static int f_raise_window(lua_State *L) { + /* + SDL_RaiseWindow should be enough but on some window managers like the + one used on Gnome the window needs to first have input focus in order + to allow the window to be focused. Also on wayland the raise window event + may not always be obeyed. + */ + SDL_SetWindowInputFocus(window); + SDL_RaiseWindow(window); + return 0; +} + static int f_show_fatal_error(lua_State *L) { const char *title = luaL_checkstring(L, 1); @@ -448,19 +551,8 @@ static int f_show_fatal_error(lua_State *L) { #ifdef _WIN32 MessageBox(0, msg, title, MB_OK | MB_ICONERROR); - #else - SDL_MessageBoxButtonData buttons[] = { - { SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, 0, "Ok" }, - }; - SDL_MessageBoxData data = { - .title = title, - .message = msg, - .numbuttons = 1, - .buttons = buttons, - }; - int buttonid; - SDL_ShowMessageBox(&data, &buttonid); + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, title, msg, NULL); #endif return 0; } @@ -517,7 +609,7 @@ static int f_list_dir(lua_State *L) { #ifdef _WIN32 lua_settop(L, 1); - if (strchr("\\/", path[strlen(path) - 2]) != NULL) + if (path[0] == 0 || strchr("\\/", path[strlen(path) - 1]) != NULL) lua_pushstring(L, "*"); else lua_pushstring(L, "/*"); @@ -843,7 +935,7 @@ typedef struct lua_function_node { #define P(FUNC) { "lua_" #FUNC, (fptr)(lua_##FUNC) } #define U(FUNC) { "luaL_" #FUNC, (fptr)(luaL_##FUNC) } static void* api_require(const char* symbol) { - static lua_function_node nodes[] = { + static const lua_function_node nodes[] = { P(atpanic), P(checkstack), P(close), P(concat), P(copy), P(createtable), P(dump), P(error), P(gc), P(getallocf), P(getfield), @@ -866,16 +958,21 @@ static void* api_require(const char* symbol) { U(newmetatable), U(setmetatable), U(testudata), U(checkudata), U(where), U(error), U(fileresult), U(execresult), U(ref), U(unref), U(loadstring), U(newstate), U(setfuncs), U(buffinit), U(addlstring), U(addstring), - U(addvalue), U(pushresult), {"api_load_libs", (void*)(api_load_libs)}, + U(addvalue), U(pushresult), U(openlibs), {"api_load_libs", (void*)(api_load_libs)}, #if LUA_VERSION_NUM >= 502 P(absindex), P(arith), P(callk), P(compare), P(getglobal), P(len), P(pcallk), P(rawgetp), P(rawlen), P(rawsetp), P(setglobal), P(iscfunction), P(yieldk), U(checkversion_), U(tolstring), U(len), U(getsubtable), U(prepbuffsize), U(pushresultsize), U(buffinitsize), U(checklstring), U(checkoption), U(gsub), U(loadbufferx), - U(loadfilex), U(optinteger), U(optlstring), U(requiref), U(traceback) + U(loadfilex), U(optinteger), U(optlstring), U(requiref), U(traceback), #else - P(objlen) + P(objlen), + #endif + #if LUA_VERSION_NUM >= 504 + P(newuserdatauv), P(setiuservalue), P(getiuservalue) + #else + P(newuserdata), P(setuservalue), P(getuservalue) #endif }; @@ -886,6 +983,14 @@ static void* api_require(const char* symbol) { return NULL; } +static int f_library_gc(lua_State *L) { + lua_getfield(L, 1, "handle"); + void* handle = lua_touserdata(L, -1); + SDL_UnloadObject(handle); + + return 0; +} + static int f_load_native_plugin(lua_State *L) { char entrypoint_name[512]; entrypoint_name[sizeof(entrypoint_name) - 1] = '\0'; int result; @@ -898,9 +1003,12 @@ static int f_load_native_plugin(lua_State *L) { lua_getglobal(L, "package"); lua_getfield(L, -1, "native_plugins"); + lua_newtable(L); lua_pushlightuserdata(L, library); + lua_setfield(L, -2, "handle"); + luaL_setmetatable(L, API_TYPE_NATIVE_PLUGIN); lua_setfield(L, -2, name); - lua_pop(L, 1); + lua_pop(L, 2); const char *basename = strrchr(name, '.'); basename = !basename ? name : basename + 1; @@ -993,7 +1101,10 @@ static const luaL_Reg lib[] = { { "set_window_hit_test", f_set_window_hit_test }, { "get_window_size", f_get_window_size }, { "set_window_size", f_set_window_size }, + { "set_text_input_rect", f_set_text_input_rect }, + { "clear_ime", f_clear_ime }, { "window_has_focus", f_window_has_focus }, + { "raise_window", f_raise_window }, { "show_fatal_error", f_show_fatal_error }, { "rmdir", f_rmdir }, { "chdir", f_chdir }, @@ -1017,6 +1128,9 @@ static const luaL_Reg lib[] = { int luaopen_system(lua_State *L) { + luaL_newmetatable(L, API_TYPE_NATIVE_PLUGIN); + lua_pushcfunction(L, f_library_gc); + lua_setfield(L, -2, "__gc"); luaL_newlib(L, lib); return 1; } diff --git a/src/api/utf8.c b/src/api/utf8.c index 6f0d6c17..b23b4cfc 100644 --- a/src/api/utf8.c +++ b/src/api/utf8.c @@ -1,19 +1,27 @@ /* * Integration of https://github.com/starwing/luautf8 * + * MIT License + * * Copyright (c) 2018 Xavier Wang * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * 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 the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. */ #include @@ -369,7 +377,7 @@ static int Lutf8_codepoint (lua_State *L) { luaL_checkstack(L, n, "string slice too long"); n = 0; /* count the number of returns */ se = s + pose; /* string end */ - for (n = 0, s += posi - 1; s < se;) { + for (s += posi - 1; s < se;) { utfint code = 0; s = utf8_safe_decode(L, s, &code); if (!lax && utf8_invalid(code)) diff --git a/src/main.c b/src/main.c index 7d014d3a..115568f5 100644 --- a/src/main.c +++ b/src/main.c @@ -5,6 +5,8 @@ #include "rencache.h" #include "renderer.h" +#include + #if defined(__amigaos4__) || defined(__morphos__) #define VSTRING "Lite XL 2.1.0r1 (10.10.2022)" #define VERSTAG "\0$VER: " VSTRING @@ -12,19 +14,20 @@ #ifdef _WIN32 #include -#elif __linux__ || __FreeBSD__ +#elif defined(__linux__) #include - #include -#elif __APPLE__ +#elif defined(__APPLE__) #include -#elif __amigaos4__ +#elif defined(__amigaos4__) #include "platform/amigaos4.h" static CONST_STRPTR stack USED = "$STACK:102400"; static CONST_STRPTR version USED = VERSTAG; -#elif __morphos__ +#elif defined(__morphos__) #include "platform/morphos.h" unsigned long __stack = 1000000; UBYTE VString[] = VERSTAG; +#elif defined(__FreeBSD__) + #include #endif @@ -46,8 +49,9 @@ static void get_exe_filename(char *buf, int sz) { buf[len] = '\0'; #elif __linux__ char path[] = "/proc/self/exe"; - int len = readlink(path, buf, sz - 1); - buf[len] = '\0'; + ssize_t len = readlink(path, buf, sz - 1); + if (len > 0) + buf[len] = '\0'; #elif __APPLE__ /* use realpath to resolve a symlink if the process was launched from one. ** This happens when Homebrew installs a cack and creates a symlink in @@ -58,8 +62,12 @@ static void get_exe_filename(char *buf, int sz) { realpath(exepath, buf); #elif defined(__amigaos4__) || defined(__morphos__) strcpy(buf, _fullpath("./lite")); +#elif __FreeBSD__ + size_t len = sz; + const int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1 }; + sysctl(mib, 4, buf, &len, NULL, 0); #else - strcpy(buf, "./lite"); + *buf = 0; #endif } @@ -98,30 +106,39 @@ void set_macos_bundle_resources(lua_State *L); #endif #ifndef LITE_ARCH_TUPLE - #if __x86_64__ || _WIN64 || __MINGW64__ + // https://learn.microsoft.com/en-us/cpp/preprocessor/predefined-macros?view=msvc-140 + #if defined(__x86_64__) || defined(_M_AMD64) || defined(__MINGW64__) #define ARCH_PROCESSOR "x86_64" - #elif __aarch64__ - #define ARCH_PROCESSOR "aarch64" #elif __arm__ #define ARCH_PROCESSOR "arm" - #elif __amigaos4__ || __morphos__ + #elif defined(__amigaos4__) || defined(__morphos__) #define ARCH_PROCESSOR "ppc" - #else + #elif defined(__i386__) || defined(_M_IX86) || defined(__MINGW32__) #define ARCH_PROCESSOR "x86" + #elif defined(__aarch64__) || defined(_M_ARM64) || defined (_M_ARM64EC) + #define ARCH_PROCESSOR "aarch64" + #elif defined(__arm__) || defined(_M_ARM) + #define ARCH_PROCESSOR "arm" #endif + #if _WIN32 #define ARCH_PLATFORM "windows" #elif __linux__ #define ARCH_PLATFORM "linux" + #elif __FreeBSD__ + #define ARCH_PLATFORM "freebsd" #elif __APPLE__ #define ARCH_PLATFORM "darwin" #elif __amigaos4__ #define ARCH_PLATFORM "amigaos4" #elif __morphos__ #define ARCH_PLATFORM "morphos" - #else + #endif + + #if !defined(ARCH_PROCESSOR) || !defined(ARCH_PLATFORM) #error "Please define -DLITE_ARCH_TUPLE." #endif + #define LITE_ARCH_TUPLE ARCH_PROCESSOR "-" ARCH_PLATFORM #endif @@ -130,7 +147,7 @@ int main(int argc, char **argv) { HINSTANCE lib = LoadLibrary("user32.dll"); int (*SetProcessDPIAware)() = (void*) GetProcAddress(lib, "SetProcessDPIAware"); SetProcessDPIAware(); -#elif defined(__morphos__) +#elif defined(__amigaos4__) || defined(__morphos__) setlocale(LC_ALL, "C"); #else signal(SIGPIPE, SIG_IGN); @@ -150,6 +167,12 @@ int main(int argc, char **argv) { #if SDL_VERSION_ATLEAST(2, 0, 5) SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1"); #endif +#if SDL_VERSION_ATLEAST(2, 0, 18) + SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1"); +#endif +#if SDL_VERSION_ATLEAST(2, 0, 22) + SDL_SetHint(SDL_HINT_IME_SUPPORT_EXTENDED_TEXT, "1"); +#endif #if SDL_VERSION_ATLEAST(2, 0, 8) /* This hint tells SDL to respect borderless window as a normal window. @@ -206,7 +229,12 @@ init_lua: char exename[2048]; get_exe_filename(exename, sizeof(exename)); - lua_pushstring(L, exename); + if (*exename) { + lua_pushstring(L, exename); + } else { + // get_exe_filename failed + lua_pushstring(L, argv[0]); + } lua_setglobal(L, "EXEFILE"); #ifdef __APPLE__ diff --git a/src/meson.build b/src/meson.build index fa4a1390..fee36d1d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -15,6 +15,8 @@ lite_sources = [ if get_option('dirmonitor_backend') == '' if cc.has_function('inotify_init', prefix : '#include') dirmonitor_backend = 'inotify' + elif host_machine.system() == 'darwin' and cc.check_header('CoreServices/CoreServices.h') + dirmonitor_backend = 'fsevents' elif cc.has_function('kqueue', prefix : '#include') dirmonitor_backend = 'kqueue' elif dependency('libkqueue', required : false).found() @@ -63,5 +65,5 @@ executable('lite-xl', link_args: lite_link_args, install_dir: lite_bindir, install: true, - gui_app: true, + win_subsystem: 'windows', ) diff --git a/src/rencache.c b/src/rencache.c index c847ce34..12c2a33c 100644 --- a/src/rencache.c +++ b/src/rencache.c @@ -2,9 +2,19 @@ #include #include #include -#include #include +#ifdef _MSC_VER + #ifndef alignof + #define alignof _Alignof + #endif + /* max_align_t is a compiler defined type, but + ** MSVC doesn't provide it, so we'll have to improvise */ + typedef long double max_align_t; +#else + #include +#endif + #include #include "rencache.h" @@ -16,7 +26,8 @@ #define CELLS_X 80 #define CELLS_Y 50 #define CELL_SIZE 96 -#define COMMAND_BUF_SIZE (1024 * 512) +#define CMD_BUF_RESIZE_RATE 1.2 +#define CMD_BUF_INIT_SIZE (1024 * 512) #define COMMAND_BARE_SIZE offsetof(Command, text) enum { SET_CLIP, DRAW_TEXT, DRAW_RECT }; @@ -29,6 +40,7 @@ typedef struct { RenColor color; RenFont *fonts[FONT_FALLBACK_MAX]; float text_x; + size_t len; char text[]; } Command; @@ -37,13 +49,15 @@ static unsigned cells_buf2[CELLS_X * CELLS_Y]; static unsigned *cells_prev = cells_buf1; static unsigned *cells = cells_buf2; static RenRect rect_buf[CELLS_X * CELLS_Y / 2]; -static char command_buf[COMMAND_BUF_SIZE]; +size_t command_buf_size = 0; +uint8_t *command_buf = NULL; +static bool resize_issue; static int command_buf_idx; static RenRect screen_rect; static bool show_debug; -static inline int min(int a, int b) { return a < b ? a : b; } -static inline int max(int a, int b) { return a > b ? a : b; } +static inline int rencache_min(int a, int b) { return a < b ? a : b; } +static inline int rencache_max(int a, int b) { return a > b ? a : b; } /* 32bit fnv-1a hash */ @@ -69,32 +83,54 @@ static inline bool rects_overlap(RenRect a, RenRect b) { static RenRect intersect_rects(RenRect a, RenRect b) { - int x1 = max(a.x, b.x); - int y1 = max(a.y, b.y); - int x2 = min(a.x + a.width, b.x + b.width); - int y2 = min(a.y + a.height, b.y + b.height); - return (RenRect) { x1, y1, max(0, x2 - x1), max(0, y2 - y1) }; + int x1 = rencache_max(a.x, b.x); + int y1 = rencache_max(a.y, b.y); + int x2 = rencache_min(a.x + a.width, b.x + b.width); + int y2 = rencache_min(a.y + a.height, b.y + b.height); + return (RenRect) { x1, y1, rencache_max(0, x2 - x1), rencache_max(0, y2 - y1) }; } static RenRect merge_rects(RenRect a, RenRect b) { - int x1 = min(a.x, b.x); - int y1 = min(a.y, b.y); - int x2 = max(a.x + a.width, b.x + b.width); - int y2 = max(a.y + a.height, b.y + b.height); + int x1 = rencache_min(a.x, b.x); + int y1 = rencache_min(a.y, b.y); + int x2 = rencache_max(a.x + a.width, b.x + b.width); + int y2 = rencache_max(a.y + a.height, b.y + b.height); return (RenRect) { x1, y1, x2 - x1, y2 - y1 }; } +static bool expand_command_buffer() { + size_t new_size = command_buf_size * CMD_BUF_RESIZE_RATE; + if (new_size == 0) { + new_size = CMD_BUF_INIT_SIZE; + } + uint8_t *new_command_buf = realloc(command_buf, new_size); + if (!new_command_buf) { + return false; + } + command_buf_size = new_size; + command_buf = new_command_buf; + return true; +} static Command* push_command(int type, int size) { - size_t alignment = alignof(max_align_t) - 1; - size = (size + alignment) & ~alignment; - Command *cmd = (Command*) (command_buf + command_buf_idx); - int n = command_buf_idx + size; - if (n > COMMAND_BUF_SIZE) { - fprintf(stderr, "Warning: (" __FILE__ "): exhausted command buffer\n"); + if (resize_issue) { + // Don't push new commands as we had problems resizing the command buffer. + // Let's wait for the next frame. return NULL; } + size_t alignment = alignof(max_align_t) - 1; + size = (size + alignment) & ~alignment; + int n = command_buf_idx + size; + while (n > command_buf_size) { + if (!expand_command_buffer()) { + fprintf(stderr, "Warning: (" __FILE__ "): unable to resize command buffer (%ld)\n", + (size_t)(command_buf_size * CMD_BUF_RESIZE_RATE)); + resize_issue = true; + return NULL; + } + } + Command *cmd = (Command*) (command_buf + command_buf_idx); command_buf_idx = n; memset(cmd, 0, size); cmd->type = type; @@ -135,12 +171,12 @@ void rencache_draw_rect(RenRect rect, RenColor color) { } } -float rencache_draw_text(RenFont **fonts, const char *text, float x, int y, RenColor color) +float rencache_draw_text(RenFont **fonts, const char *text, size_t len, float x, int y, RenColor color) { - float width = ren_font_group_get_width(fonts, text); + float width = ren_font_group_get_width(fonts, text, len); RenRect rect = { x, y, (int)width, ren_font_group_get_height(fonts) }; if (rects_overlap(screen_rect, rect)) { - int sz = strlen(text) + 1; + int sz = len + 1; Command *cmd = push_command(DRAW_TEXT, COMMAND_BARE_SIZE + sz); if (cmd) { memcpy(cmd->text, text, sz); @@ -148,6 +184,7 @@ float rencache_draw_text(RenFont **fonts, const char *text, float x, int y, RenC memcpy(cmd->fonts, fonts, sizeof(RenFont*)*FONT_FALLBACK_MAX); cmd->rect = rect; cmd->text_x = x; + cmd->len = len; cmd->tab_size = ren_font_group_get_tab_size(fonts); } } @@ -163,6 +200,7 @@ void rencache_invalidate(void) { void rencache_begin_frame() { /* reset all cells if the screen width/height has changed */ int w, h; + resize_issue = false; ren_get_size(&w, &h); if (screen_rect.width != w || h != screen_rect.height) { screen_rect.width = w; @@ -256,7 +294,7 @@ void rencache_end_frame() { break; case DRAW_TEXT: ren_font_group_set_tab_size(cmd->fonts, cmd->tab_size); - ren_draw_text(cmd->fonts, cmd->text, cmd->text_x, cmd->rect.y, cmd->color); + ren_draw_text(cmd->fonts, cmd->text, cmd->len, cmd->text_x, cmd->rect.y, cmd->color); break; } } diff --git a/src/rencache.h b/src/rencache.h index 251bc030..d2947b88 100644 --- a/src/rencache.h +++ b/src/rencache.h @@ -8,7 +8,7 @@ void rencache_show_debug(bool enable); void rencache_set_clip_rect(RenRect rect); void rencache_draw_rect(RenRect rect, RenColor color); -float rencache_draw_text(RenFont **font, const char *text, float x, int y, RenColor color); +float rencache_draw_text(RenFont **font, const char *text, size_t len, float x, int y, RenColor color); void rencache_invalidate(void); void rencache_begin_frame(); void rencache_end_frame(); diff --git a/src/renderer.c b/src/renderer.c index 30592852..ee814bd4 100644 --- a/src/renderer.c +++ b/src/renderer.c @@ -8,6 +8,11 @@ #include #include FT_FREETYPE_H +#ifdef _WIN32 +#include +#include "utfconv.h" +#endif + #include "renderer.h" #include "renwindow.h" @@ -51,7 +56,11 @@ typedef struct RenFont { ERenFontHinting hinting; unsigned char style; unsigned short underline_thickness; - char path[1]; +#ifdef _WIN32 + unsigned char *file; + HANDLE file_handle; +#endif + char path[]; } RenFont; static const char* utf8_to_codepoint(const char *p, unsigned *dst) { @@ -206,9 +215,48 @@ static void font_clear_glyph_cache(RenFont* font) { } RenFont* ren_font_load(const char* path, float size, ERenFontAntialiasing antialiasing, ERenFontHinting hinting, unsigned char style) { - FT_Face face; - if (FT_New_Face( library, path, 0, &face)) + FT_Face face = NULL; + +#ifdef _WIN32 + + HANDLE file = INVALID_HANDLE_VALUE; + DWORD read; + int font_file_len = 0; + unsigned char *font_file = NULL; + wchar_t *wpath = NULL; + + if ((wpath = utfconv_utf8towc(path)) == NULL) return NULL; + + if ((file = CreateFileW(wpath, + GENERIC_READ, + FILE_SHARE_READ, // or else we can't copy fonts + NULL, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + NULL)) == INVALID_HANDLE_VALUE) + goto failure; + + if ((font_file_len = GetFileSize(file, NULL)) == INVALID_FILE_SIZE) + goto failure; + + font_file = check_alloc(malloc(font_file_len * sizeof(unsigned char))); + if (!ReadFile(file, font_file, font_file_len, &read, NULL) || read != font_file_len) + goto failure; + + free(wpath); + wpath = NULL; + + if (FT_New_Memory_Face(library, font_file, read, 0, &face)) + goto failure; + +#else + + if (FT_New_Face(library, path, 0, &face)) + return NULL; + +#endif + const int surface_scale = renwin_surface_scale(&window_renderer); if (FT_Set_Pixel_Sizes(face, 0, (int)(size*surface_scale))) goto failure; @@ -223,17 +271,32 @@ RenFont* ren_font_load(const char* path, float size, ERenFontAntialiasing antial font->hinting = hinting; font->style = style; +#ifdef _WIN32 + // we need to keep this for freetype + font->file = font_file; + font->file_handle = file; +#endif + if(FT_IS_SCALABLE(face)) font->underline_thickness = (unsigned short)((face->underline_thickness / (float)face->units_per_EM) * font->size); if(!font->underline_thickness) font->underline_thickness = ceil((double) font->height / 14.0); - if (FT_Load_Char(face, ' ', font_set_load_options(font))) + if (FT_Load_Char(face, ' ', font_set_load_options(font))) { + free(font); goto failure; + } font->space_advance = face->glyph->advance.x / 64.0f; font->tab_advance = font->space_advance * 2; return font; - failure: - FT_Done_Face(face); + +failure: +#ifdef _WIN32 + free(wpath); + free(font_file); + if (file != INVALID_HANDLE_VALUE) CloseHandle(file); +#endif + if (face != NULL) + FT_Done_Face(face); return NULL; } @@ -252,6 +315,10 @@ const char* ren_font_get_path(RenFont *font) { void ren_font_free(RenFont* font) { font_clear_glyph_cache(font); FT_Done_Face(font->face); +#ifdef _WIN32 + free(font->file); + CloseHandle(font->file_handle); +#endif free(font); } @@ -293,9 +360,9 @@ int ren_font_group_get_height(RenFont **fonts) { return fonts[0]->height; } -float ren_font_group_get_width(RenFont **fonts, const char *text) { +float ren_font_group_get_width(RenFont **fonts, const char *text, size_t len) { float width = 0; - const char* end = text + strlen(text); + const char* end = text + len; GlyphMetric* metric = NULL; GlyphSet* set = NULL; while (text < end) { unsigned int codepoint; @@ -309,7 +376,7 @@ float ren_font_group_get_width(RenFont **fonts, const char *text) { return width / surface_scale; } -float ren_draw_text(RenFont **fonts, const char *text, float x, int y, RenColor color) { +float ren_draw_text(RenFont **fonts, const char *text, size_t len, float x, int y, RenColor color) { SDL_Surface *surface = renwin_get_surface(&window_renderer); const RenRect clip = window_renderer.clip; @@ -317,11 +384,11 @@ float ren_draw_text(RenFont **fonts, const char *text, float x, int y, RenColor float pen_x = x * surface_scale; y *= surface_scale; int bytes_per_pixel = surface->format->BytesPerPixel; - const char* end = text + strlen(text); + const char* end = text + len; uint8_t* destination_pixels = surface->pixels; int clip_end_x = clip.x + clip.width, clip_end_y = clip.y + clip.height; - RenFont* last; + RenFont* last = NULL; float last_pen_x = x; bool underline = fonts[0]->style & FONT_STYLE_UNDERLINE; bool strikethrough = fonts[0]->style & FONT_STYLE_STRIKETHROUGH; @@ -458,8 +525,13 @@ void ren_draw_rect(RenRect rect, RenColor color) { /*************** Window Management ****************/ void ren_free_window_resources() { + extern uint8_t *command_buf; + extern size_t command_buf_size; renwin_free(&window_renderer); SDL_FreeSurface(draw_rect_surface); + free(command_buf); + command_buf = NULL; + command_buf_size = 0; } void ren_init(SDL_Window *win) { diff --git a/src/renderer.h b/src/renderer.h index 3e631ce4..c1b80f51 100644 --- a/src/renderer.h +++ b/src/renderer.h @@ -11,7 +11,7 @@ #define UNUSED #endif -#define FONT_FALLBACK_MAX 4 +#define FONT_FALLBACK_MAX 10 typedef struct RenFont RenFont; typedef enum { FONT_HINTING_NONE, FONT_HINTING_SLIGHT, FONT_HINTING_FULL } ERenFontHinting; typedef enum { FONT_ANTIALIASING_NONE, FONT_ANTIALIASING_GRAYSCALE, FONT_ANTIALIASING_SUBPIXEL } ERenFontAntialiasing; @@ -28,8 +28,8 @@ int ren_font_group_get_height(RenFont **font); float ren_font_group_get_size(RenFont **font); void ren_font_group_set_size(RenFont **font, float size); void ren_font_group_set_tab_size(RenFont **font, int n); -float ren_font_group_get_width(RenFont **font, const char *text); -float ren_draw_text(RenFont **font, const char *text, float x, int y, RenColor color); +float ren_font_group_get_width(RenFont **font, const char *text, size_t len); +float ren_draw_text(RenFont **font, const char *text, size_t len, float x, int y, RenColor color); void ren_draw_rect(RenRect rect, RenColor color); diff --git a/src/utfconv.h b/src/utfconv.h index 059b3071..59f98e4a 100644 --- a/src/utfconv.h +++ b/src/utfconv.h @@ -1,6 +1,12 @@ #ifndef MBSEC_H #define MBSEC_H +#ifdef __GNUC__ +#define UNUSED __attribute__((__unused__)) +#else +#define UNUSED +#endif + #ifdef _WIN32 #include @@ -8,7 +14,7 @@ #define UTFCONV_ERROR_INVALID_CONVERSION "Input contains invalid byte sequences." -LPWSTR utfconv_utf8towc(const char *str) { +static UNUSED LPWSTR utfconv_utf8towc(const char *str) { LPWSTR output; int len; @@ -30,7 +36,7 @@ LPWSTR utfconv_utf8towc(const char *str) { return output; } -char *utfconv_wctoutf8(LPCWSTR str) { +static UNUSED char *utfconv_wctoutf8(LPCWSTR str) { char *output; int len; diff --git a/subprojects/freetype2.wrap b/subprojects/freetype2.wrap index 6d7f449a..1ff89ecd 100644 --- a/subprojects/freetype2.wrap +++ b/subprojects/freetype2.wrap @@ -1,9 +1,10 @@ [wrap-file] -directory = freetype-2.11.1 -source_url = https://download.savannah.gnu.org/releases/freetype/freetype-2.11.1.tar.gz -source_filename = freetype-2.11.1.tar.gz -source_hash = f8db94d307e9c54961b39a1cc799a67d46681480696ed72ecf78d4473770f09b +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 [provide] freetype2 = freetype_dep - +freetype = freetype_dep diff --git a/subprojects/lua.wrap b/subprojects/lua.wrap index bd6ee5eb..5a2d615b 100644 --- a/subprojects/lua.wrap +++ b/subprojects/lua.wrap @@ -1,11 +1,11 @@ [wrap-file] -directory = lua-5.4.3 -source_url = https://www.lua.org/ftp/lua-5.4.3.tar.gz -source_filename = lua-5.4.3.tar.gz -source_hash = f8612276169e3bfcbcfb8f226195bfc6e466fe13042f1076cbde92b7ec96bbfb -patch_filename = lua_5.4.3-2_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/lua_5.4.3-2/get_patch -patch_hash = 3c23ec14a3f000d80fe2e2fdddba63a56e13c758d74195daa4ff0da7bfdb02da +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 [provide] lua-5.4 = lua_dep diff --git a/subprojects/pcre2.wrap b/subprojects/pcre2.wrap index 99e82cf4..8fada34e 100644 --- a/subprojects/pcre2.wrap +++ b/subprojects/pcre2.wrap @@ -1,15 +1,15 @@ [wrap-file] -directory = pcre2-10.39 -source_url = https://github.com/PhilipHazel/pcre2/releases/download/pcre2-10.39/pcre2-10.39.tar.bz2 -source_filename = pcre2-10.39.tar.bz2 -source_hash = 0f03caf57f81d9ff362ac28cd389c055ec2bf0678d277349a1a4bee00ad6d440 -patch_filename = pcre2_10.39-2_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/pcre2_10.39-2/get_patch -patch_hash = c4cfffff83e7bb239c8c330339b08f4367b019f79bf810f10c415e35fb09cf14 +directory = pcre2-10.42 +source_url = https://github.com/PhilipHazel/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 [provide] -libpcre2-8 = -libpcre2_8 -libpcre2-16 = -libpcre2_16 -libpcre2-32 = -libpcre2_32 -libpcre2-posix = -libpcre2_posix - +libpcre2-8 = libpcre2_8 +libpcre2-16 = libpcre2_16 +libpcre2-32 = libpcre2_32 +libpcre2-posix = libpcre2_posix diff --git a/subprojects/sdl2.wrap b/subprojects/sdl2.wrap index aafa1fcc..53a71e2f 100644 --- a/subprojects/sdl2.wrap +++ b/subprojects/sdl2.wrap @@ -1,12 +1,12 @@ [wrap-file] -directory = SDL2-2.24.0 -source_url = https://libsdl.org/release/SDL2-2.24.0.tar.gz -source_filename = SDL2-2.24.0.tar.gz -source_hash = 91e4c34b1768f92d399b078e171448c6af18cafda743987ed2064a28954d6d97 -patch_filename = sdl2_2.24.0-2_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/sdl2_2.24.0-2/get_patch -patch_hash = ec296ed9a577b42131d2fdbfe5ca73a0cf133793c0290e1ccd825675464bfe32 -wrapdb_version = 2.24.0-2 +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 [provide] sdl2 = sdl2_dep