diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 398aae67..21f3a8b4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -110,6 +110,12 @@ jobs: run: | echo "$HOME/.local/bin" >> "$GITHUB_PATH" echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-macos-universal" >> "$GITHUB_ENV" + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + - name: Install dmgbuild + run: pip install dmgbuild - uses: actions/checkout@v3 - name: Download artifacts uses: actions/download-artifact@v3 @@ -117,8 +123,6 @@ jobs: with: name: macOS DMG Images path: dmgs-original - - name: Install appdmg - run: cd ~; npm i appdmg; cd - - name: Make universal bundles run: | bash --version @@ -200,13 +204,14 @@ jobs: run: | "INSTALL_NAME=lite-xl-$($env:GITHUB_REF -replace ".*/")-windows-msvc-${{ matrix.arch.name }}" >> $env:GITHUB_ENV "INSTALL_REF=$($env:GITHUB_REF -replace ".*/")" >> $env:GITHUB_ENV - "LUA_SUBPROJECT_PATH=subprojects/lua-5.4.4" >> $env:GITHUB_ENV + "LUA_SUBPROJECT_PATH=subprojects/$(awk -F ' *= *' '/directory/ { printf $2 }' subprojects/lua.wrap)" >> $env:GITHUB_ENV + - name: Download and patch subprojects + shell: bash + run: | + meson subprojects download + cat resources/windows/001-lua-unicode.diff | patch -Np1 -d "$LUA_SUBPROJECT_PATH" - name: Configure run: | - # Download the subprojects first so we can patch it before configuring. - # This avoids reconfiguring the subprojects when compiling. - meson subprojects download - Get-Content -Path resources/windows/001-lua-unicode.diff -Raw | patch -d $env:LUA_SUBPROJECT_PATH -p1 --forward meson setup --wrap-mode=forcefallback build - name: Build run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index edf66372..5d9a388c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -185,8 +185,8 @@ jobs: uses: actions/setup-python@v2 with: python-version: 3.9 - - name: Install appdmg - run: cd ~; npm i appdmg; cd - + - name: Install dmgbuild + run: pip install dmgbuild - name: Prepare DMG Images run: | mkdir -p dmgs-addons dmgs-normal diff --git a/.gitignore b/.gitignore index d3a6f771..80e09311 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ lite !resources/windows/*.diff !resources/windows/*.exe.manifest.in +!resources/macos/*.py diff --git a/LICENSE b/LICENSE index da7be0e2..387fd09d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2020-2021 Lite XL Team +Copyright (c) 2020-present Lite XL Team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/README.md b/README.md index d7c7e3c3..05949e80 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Lite XL [![CI]](https://github.com/lite-xl/lite-xl/actions/workflows/build.yml) -[![Discord Badge Image]](https://discord.gg/RWzqC3nx7K) +[![Discord Badge Image]](https://discord.gg/UQKnzBhY5H) ![screenshot-dark] @@ -81,6 +81,39 @@ affects only the place where the application is actually installed. Head over to [releases](https://github.com/lite-xl/lite-xl/releases) and download the version for your operating system. +### Windows + +Lite XL comes with installers on Windows for typical installations. +Alternatively, we provide ZIP archives that you can download and extract anywhere and run directly. + +To make Lite XL portable (e.g. running Lite XL from a thumb drive), +simply create a `user` folder where `lite-xl.exe` is located. +Lite XL will load and store all your configurations and plugins in the folder. + +### macOS + +We provide DMG files for macOS. Simply drag the program into your Applications folder. + +> **Important** +> Newer versions of Lite XL are signed with a self-signed certificate, +> so you'll have to follow these steps when running Lite XL for the first time. +> +> 1. Find Lite XL in Finder (do not open it in Launchpad). +> 2. Control-click Lite XL, then choose `Open` from the shortcut menu. +> 3. Click `Open` in the popup menu. +> +> The correct steps may vary between macOS versions, so you should refer to +> the [macOS User Guide](https://support.apple.com/en-my/guide/mac-help/mh40616/mac). +> +> On an older version of Lite XL, you will need to run these commands instead: +> +> ```sh +> # clears attributes from the directory +> xattr -cr /Applications/Lite\ XL.app +> ``` +> +> Otherwise, macOS will display a **very misleading error** saying that the application is damaged. + ### Linux Unzip the file and `cd` into the `lite-xl` directory: @@ -91,6 +124,7 @@ cd lite-xl ``` To run lite-xl without installing: + ```sh ./lite-xl ``` @@ -103,21 +137,59 @@ mkdir -p $HOME/.local/bin && cp lite-xl $HOME/.local/bin/ mkdir -p $HOME/.local/share/lite-xl && cp -r data/* $HOME/.local/share/lite-xl/ ``` +#### Add Lite XL to PATH + +To run Lite XL from the command line, you must add it to PATH. + If `$HOME/.local/bin` is not in PATH: ```sh echo -e 'export PATH=$PATH:$HOME/.local/bin' >> $HOME/.bashrc ``` -To get the icon to show up in app launcher: +Alternatively on recent versions of GNOME and KDE Plasma, +you can add `$HOME/.local/bin` to PATH via `~/.config/environment.d/envvars.conf`: + +```ini +PATH=$HOME/.local/bin:$PATH +``` + +> **Note** +> Some systems might not load `.bashrc` when logging in. +> This can cause problems with launching applications from the desktop / menu. + +#### Add Lite XL to application launchers + +To get the icon to show up in app launcher, you need to create a desktop +entry and put it into `/usr/share/applications` or `~/.local/share/applications`. + +Here is an example for a desktop entry in `~/.local/share/applications/com.lite_xl.LiteXL.desktop`, +assuming Lite XL is in PATH: + +```ini +[Desktop Entry] +Type=Application +Name=Lite XL +Comment=A lightweight text editor written in Lua +Exec=lite-xl %F +Icon=lite-xl +Terminal=false +StartupWMClass=lite-xl +Categories=Development;IDE; +MimeType=text/plain;inode/directory; +``` + +To get the icon to show up in app launcher immediately, run: ```sh xdg-desktop-menu forceupdate ``` -You may need to logout and login again to see icon in app launcher. +Alternatively, you may log out and log in again. -To uninstall just run: +#### Uninstall + +To uninstall Lite XL, run: ```sh rm -f $HOME/.local/bin/lite-xl @@ -127,7 +199,6 @@ rm -rf $HOME/.local/share/icons/hicolor/scalable/apps/lite-xl.svg \ $HOME/.local/share/lite-xl ``` - ## Contributing Any additional functionality that can be added through a plugin should be done diff --git a/build-packages.sh b/build-packages.sh index 96a7d11d..a95d3460 100755 --- a/build-packages.sh +++ b/build-packages.sh @@ -38,7 +38,7 @@ show_help() { echo "-v --version VERSION Sets the version on the package name." echo "-A --appimage Create an AppImage (Linux only)." echo "-D --dmg Create a DMG disk image (macOS only)." - echo " Requires NPM and AppDMG." + echo " Requires dmgbuild." echo "-I --innosetup Create an InnoSetup installer (Windows only)." echo "-r --release Compile in release mode." echo "-S --source Create a source code package," diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index aa73a2c0..aca5fc65 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -43,7 +43,7 @@ local function save(filename) core.log("Saved \"%s\"", saved_filename) else core.error(err) - core.nag_view:show("Saving failed", string.format("Could not save \"%s\" do you want to save to another location?", doc().filename), { + core.nag_view:show("Saving failed", string.format("Couldn't save file \"%s\". Do you want to save to another location?", doc().filename), { { text = "No", default_no = true }, { text = "Yes", default_yes = true } }, function(item) @@ -340,10 +340,11 @@ local commands = { local text = dv.doc:get_text(line1, 1, line1, col1) if #text >= indent_size and text:find("^ *$") then dv.doc:delete_to_cursor(idx, 0, -indent_size) - return + goto continue end end dv.doc:delete_to_cursor(idx, translate.previous_char) + ::continue:: end end, @@ -544,6 +545,11 @@ local commands = { dv.doc.crlf = not dv.doc.crlf end, + ["doc:toggle-overwrite"] = function(dv) + dv.doc.overwrite = not dv.doc.overwrite + core.blink_reset() -- to show the cursor has changed edit modes + end, + ["doc:save-as"] = function(dv) local last_doc = core.last_active_view and core.last_active_view.doc local text diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua index a3151cb7..b77bbcf2 100644 --- a/data/core/commands/findreplace.lua +++ b/data/core/commands/findreplace.lua @@ -164,13 +164,16 @@ local function is_in_any_selection(line, col) end local function select_add_next(all) - local il1, ic1 = doc():get_selection(true) - for idx, l1, c1, l2, c2 in doc():get_selections(true, true) do + local il1, ic1 + for _, l1, c1, l2, c2 in doc():get_selections(true, true) do + if not il1 then + il1, ic1 = l1, c1 + end local text = doc():get_text(l1, c1, l2, c2) repeat l1, c1, l2, c2 = search.find(doc(), l2, c2, text, { wrap = true }) if l1 == il1 and c1 == ic1 then break end - if l2 and (all or not is_in_any_selection(l2, c2)) then + if l2 and not is_in_any_selection(l2, c2) then doc():add_selection(l2, c2, l1, c1) if not all then core.active_view:scroll_to_make_visible(l2, c2) @@ -266,7 +269,7 @@ command.add(valid_for_finding, { core.error("No find to continue from") else local sl1, sc1, sl2, sc2 = dv.doc:get_selection(true) - local line1, col1, line2, col2 = last_fn(dv.doc, sl1, sc2, last_text, case_sensitive, find_regex, false) + local line1, col1, line2, col2 = last_fn(dv.doc, sl2, sc2, last_text, case_sensitive, find_regex, false) if line1 then dv.doc:set_selection(line2, col2, line1, col1) dv:scroll_to_line(line2, true) diff --git a/data/core/common.lua b/data/core/common.lua index b525b4f7..9aa0aa31 100644 --- a/data/core/common.lua +++ b/data/core/common.lua @@ -226,7 +226,7 @@ function common.path_suggest(text, root) if root and root:sub(-1) ~= PATHSEP then root = root .. PATHSEP end - local path, name = text:match("^(.-)([^:/\\]*)$") + local path, name = text:match("^(.-)([^"..PATHSEP.."]*)$") local clean_dotslash = false -- ignore root if path is absolute local is_absolute = common.is_absolute_path(text) @@ -279,7 +279,7 @@ end ---@param text string The input path. ---@return string[] function common.dir_path_suggest(text) - local path, name = text:match("^(.-)([^:/\\]*)$") + local path, name = text:match("^(.-)([^"..PATHSEP.."]*)$") local files = system.list_dir(path == "" and "." or path) or {} local res = {} for _, file in ipairs(files) do @@ -298,7 +298,7 @@ end ---@param dir_list string[] A list of paths to filter. ---@return string[] function common.dir_list_suggest(text, dir_list) - local path, name = text:match("^(.-)([^:/\\]*)$") + local path, name = text:match("^(.-)([^"..PATHSEP.."]*)$") local res = {} for _, dir_path in ipairs(dir_list) do if dir_path:lower():find(text:lower(), nil, true) == 1 then @@ -378,12 +378,15 @@ function common.bench(name, fn, ...) return res end +-- From gvx/Ser +local oddvals = {[tostring(1/0)] = "1/0", [tostring(-1/0)] = "-1/0", [tostring(-(0/0))] = "-(0/0)", [tostring(0/0)] = "0/0"} local function serialize(val, pretty, indent_str, escape, sort, limit, level) local space = pretty and " " or "" local indent = pretty and string.rep(indent_str, level) or "" local newline = pretty and "\n" or "" - if type(val) == "string" then + local ty = type(val) + if ty == "string" then local out = string.format("%q", val) if escape then out = string.gsub(out, "\\\n", "\\n") @@ -395,7 +398,7 @@ local function serialize(val, pretty, indent_str, escape, sort, limit, level) out = string.gsub(out, "\\13", "\\r") end return out - elseif type(val) == "table" then + elseif ty == "table" then -- early exit if level >= limit then return tostring(val) end local next_indent = pretty and (indent .. indent_str) or "" @@ -410,6 +413,12 @@ local function serialize(val, pretty, indent_str, escape, sort, limit, level) if sort then table.sort(t) end return "{" .. newline .. table.concat(t, "," .. newline) .. newline .. indent .. "}" end + if ty == "number" then + -- tostring is locale-dependent, so we need to replace an eventual `,` with `.` + local res, _ = tostring(val):gsub(",", ".") + -- handle inf/nan + return oddvals[res] or res + end return tostring(val) end @@ -452,7 +461,7 @@ end function common.basename(path) -- a path should never end by / or \ except if it is '/' (unix root) or -- 'X:\' (windows drive) - return path:match("[^\\/]+$") or path + return path:match("[^"..PATHSEP.."]+$") or path end @@ -461,7 +470,7 @@ end ---@param path string ---@return string|nil function common.dirname(path) - return path:match("(.+)[:\\/][^\\/]+$") + return path:match("(.+)["..PATHSEP.."][^"..PATHSEP.."]+$") end @@ -507,10 +516,10 @@ end local function split_on_slash(s, sep_pattern) local t = {} - if s:match("^[/\\]") then + if s:match("^["..PATHSEP.."]") then t[#t + 1] = "" end - for fragment in string.gmatch(s, "([^/\\]+)") do + for fragment in string.gmatch(s, "([^"..PATHSEP.."]+)") do t[#t + 1] = fragment end return t @@ -643,7 +652,7 @@ function common.mkdirp(path) while path and path ~= "" do local success_mkdir = system.mkdir(path) if success_mkdir then break end - local updir, basedir = path:match("(.*)[/\\](.+)$") + local updir, basedir = path:match("(.*)["..PATHSEP.."](.+)$") table.insert(subdirs, 1, basedir or path) path = updir end diff --git a/data/core/config.lua b/data/core/config.lua index c1a16b11..88f8d96d 100644 --- a/data/core/config.lua +++ b/data/core/config.lua @@ -2,15 +2,71 @@ local common = require "core.common" local config = {} +---The frame rate of Lite XL. +---Note that setting this value to the screen's refresh rate +---does not eliminate screen tearing. +--- +---Defaults to 60. +---@type number config.fps = 60 + +---Maximum number of log items that will be stored. +---When the number of log items exceed this value, old items will be discarded. +--- +---Defaults to 800. +---@type number config.max_log_items = 800 + +---The timeout, in seconds, before a message dissapears from StatusView. +--- +---Defaults to 5. +---@type number config.message_timeout = 5 + +---The number of pixels scrolled per-step. +--- +---Defaults to 50 * SCALE. +---@type number config.mouse_wheel_scroll = 50 * SCALE + +---Enables/disables transitions when scrolling with the scrollbar. +---When enabled, the scrollbar will have inertia and slowly move towards the cursor. +---Otherwise, the scrollbar will immediately follow the cursor. +--- +---Defaults to false. +---@type boolean config.animate_drag_scroll = false + +---Enables/disables scrolling past the end of a document. +--- +---Defaults to true. +---@type boolean config.scroll_past_end = true ----@type "expanded" | "contracted" | false @Force the scrollbar status of the DocView + +---@alias config.scrollbartype +---| "expanded" # A thicker scrollbar is shown at all times. +---| "contracted" # A thinner scrollbar is shown at all times. +---| false # The scrollbar expands when the cursor hovers over it. + +---Controls whether the DocView scrollbar is always shown or hidden. +---This option does not affect other View's scrollbars. +--- +---Defaults to false. +---@type config.scrollbartype config.force_scrollbar_status = false + +---The file size limit, in megabytes. +---Files larger than this size will not be shown in the file picker. +--- +---Defaults to 10. +---@type number config.file_size_limit = 10 + +---A list of files and directories to ignore. +---Each element is a Lua pattern, where patterns ending with a forward slash +---are recognized as directories while patterns ending with an anchor ("$") are +---recognized as files. +---@type string[] config.ignore_files = { -- folders "^%.svn/", "^%.git/", "^%.hg/", "^CVS/", "^%.Trash/", "^%.Trash%-.*/", @@ -21,46 +77,194 @@ config.ignore_files = { "%.suo$", "%.pdb$", "%.idb$", "%.class$", "%.psd$", "%.db$", "^desktop%.ini$", "^%.DS_Store$", "^%.directory$", } + +---Lua pattern used to find symbols when advanced syntax highlighting +---is not available. +---This pattern is also used for navigation, e.g. move to next word. +--- +---The default pattern matches all letters, followed by any number +---of letters and digits. +---@type string config.symbol_pattern = "[%a_][%w_]*" + +---A list of characters that delimits a word. +--- +---The default is ``" \t\n/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-"`` +---@type string config.non_word_chars = " \t\n/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-" + +---The timeout, in seconds, before several consecutive actions +---are merged as a single undo step. +--- +---The default is 0.3 seconds. +---@type number config.undo_merge_timeout = 0.3 + +---The maximum number of undo steps per-document. +--- +---The default is 10000. +---@type number config.max_undos = 10000 + +---The maximum number of tabs shown at a time. +--- +---The default is 8. +---@type number config.max_tabs = 8 + +---Shows/hides the tab bar when there is only one tab open. +--- +---The tab bar is always shown by default. +---@type boolean config.always_show_tabs = true --- Possible values: false, true, "no_selection" + +---@alias config.highlightlinetype +---| true # Always highlight the current line. +---| false # Never highlight the current line. +---| "no_selection" # Highlight the current line if no text is selected. + +---Highlights the current line. +--- +---The default is true. +---@type config.highlightlinetype config.highlight_current_line = true + +---The spacing between each line of text. +--- +---The default is 120% of the height of the text (1.2). +---@type number config.line_height = 1.2 + +---The number of spaces each level of indentation represents. +--- +---The default is 2. +---@type number config.indent_size = 2 + +---The type of indentation. +--- +---The default is "soft" (spaces). +---@type "soft" | "hard" config.tab_type = "soft" + +---Do not remove whitespaces when advancing to the next line. +--- +---Defaults to false. +---@type boolean config.keep_newline_whitespace = false + +---Default line endings for new files. +--- +---Defaults to `crlf` (`\r\n`) on Windows and `lf` (`\n`) on everything else. +---@type "crlf" | "lf" +config.line_endings = PLATFORM == "Windows" and "crlf" or "lf" + +---Maximum number of characters per-line for the line guide. +--- +---Defaults to 80. +---@type number config.line_limit = 80 + +---Maximum number of project files to keep track of. +---If the number of files in the project exceeds this number, +---Lite XL will not be able to keep track of them. +---They will be not be searched when searching for files or text. +--- +---Defaults to 2000. +---@type number config.max_project_files = 2000 + +---Enables/disables all transitions. +--- +---Defaults to true. +---@type boolean config.transitions = true + +---Enable/disable individual transitions. +---These values are overriden by `config.transitions`. config.disabled_transitions = { + ---Disables scrolling transitions. scroll = false, + ---Disables transitions for CommandView's suggestions list. commandview = false, + ---Disables transitions for showing/hiding the context menu. contextmenu = false, + ---Disables transitions when clicking on log items in LogView. logview = false, + ---Disables transitions for showing/hiding the Nagbar. nagbar = false, + ---Disables transitions when scrolling the tab bar. tabs = false, + ---Disables transitions when a tab is being dragged. tab_drag = false, + ---Disables transitions when a notification is shown. statusbar = false, } + +---The rate of all transitions. +--- +---Defaults to 1. +---@type number config.animation_rate = 1.0 + +---The caret's blinking period, in seconds. +--- +---Defaults to 0.8. +---@type number config.blink_period = 0.8 + +---Disables caret blinking. +--- +---Defaults to false. +---@type boolean config.disable_blink = false + +---Draws whitespaces as dots. +---This option is deprecated. +---Please use the drawwhitespace plugin instead. +---@deprecated config.draw_whitespace = false + +---Disables system-drawn window borders. +--- +---When set to true, Lite XL draws its own window decorations, +---which can be useful for certain setups. +--- +---Defaults to false. +---@type boolean config.borderless = false + +---Shows/hides the close buttons on tabs. +---When hidden, users can close tabs via keyboard shortcuts or commands. +--- +---Defaults to true. +---@type boolean config.tab_close_button = true + +---Maximum number of clicks recognized by Lite XL. +--- +---Defaults to 3. +---@type number config.max_clicks = 3 --- set as true to be able to test non supported plugins +---Disables plugin version checking. +---Do not change this unless you know what you are doing. +--- +---Defaults to false. +---@type boolean config.skip_plugins_version = false -- holds the plugins real config table local plugins_config = {} --- virtual representation of plugins config table +---A table containing configuration for all the plugins. +--- +---This is a metatable that automaticaly creates a minimal +---configuration when a plugin is initially configured. +---Each plugins will then call `common.merge()` to get the finalized +---plugin config. +---Do not use raw operations on this table. +---@type table config.plugins = {} -- allows virtual access to the plugins config table diff --git a/data/core/contextmenu.lua b/data/core/contextmenu.lua index 4b466907..d59609d7 100644 --- a/data/core/contextmenu.lua +++ b/data/core/contextmenu.lua @@ -12,11 +12,31 @@ local divider_width = 1 local divider_padding = 5 local DIVIDER = {} +---An item in the context menu. +---@class core.contextmenu.item +---@field text string +---@field info string|nil If provided, this text is displayed on the right side of the menu. +---@field command string|fun() + +---A list of items with the same predicate. +---@see core.command.predicate +---@class core.contextmenu.itemset +---@field predicate core.command.predicate +---@field items core.contextmenu.item[] + +---A context menu. ---@class core.contextmenu : core.object +---@field itemset core.contextmenu.itemset[] +---@field show_context_menu boolean +---@field selected number +---@field position core.view.position +---@field current_scale number local ContextMenu = Object:extend() +---A unique value representing the divider in a context menu. ContextMenu.DIVIDER = DIVIDER +---Creates a new context menu. function ContextMenu:new() self.itemset = {} self.show_context_menu = false @@ -55,12 +75,19 @@ local function update_items_size(items, update_binding) items.width, items.height = width, height end +---Registers a list of items into the context menu with a predicate. +---@param predicate core.command.predicate +---@param items core.contextmenu.item[] function ContextMenu:register(predicate, items) predicate = command.generate_predicate(predicate) update_items_size(items, true) table.insert(self.itemset, { predicate = predicate, items = items }) end +---Shows the context menu. +---@param x number +---@param y number +---@return boolean # If true, the context menu is shown. function ContextMenu:show(x, y) self.items = nil local items_list = { width = 0, height = 0 } @@ -94,6 +121,7 @@ function ContextMenu:show(x, y) return false end +---Hides the context menu. function ContextMenu:hide() self.show_context_menu = false self.items = nil @@ -102,6 +130,8 @@ function ContextMenu:hide() core.request_cursor(core.active_view.cursor) end +---Returns an iterator that iterates over each context menu item and their dimensions. +---@return fun(): number, core.contextmenu.item, number, number, number, number function ContextMenu:each_item() local x, y, w = self.position.x, self.position.y, self.items.width local oy = y @@ -115,8 +145,12 @@ function ContextMenu:each_item() end) end +---Event handler for mouse movements. +---@param px any +---@param py any +---@return boolean # true if the event is caught. function ContextMenu:on_mouse_moved(px, py) - if not self.show_context_menu then return end + if not self.show_context_menu then return false end self.selected = -1 for i, item, x, y, w, h in self:each_item() do @@ -128,6 +162,8 @@ function ContextMenu:on_mouse_moved(px, py) return true end +---Event handler for when the selection is confirmed. +---@param item core.contextmenu.item function ContextMenu:on_selected(item) if type(item.command) == "string" then command.perform(item.command) @@ -140,6 +176,7 @@ local function change_value(value, change) return value + change end +---Selects the the previous item. function ContextMenu:focus_previous() self.selected = (self.selected == -1 or self.selected == 1) and #self.items or change_value(self.selected, -1) if self:get_item_selected() == DIVIDER then @@ -147,6 +184,7 @@ function ContextMenu:focus_previous() end end +---Selects the next item. function ContextMenu:focus_next() self.selected = (self.selected == -1 or self.selected == #self.items) and 1 or change_value(self.selected, 1) if self:get_item_selected() == DIVIDER then @@ -154,10 +192,13 @@ function ContextMenu:focus_next() end end +---Gets the currently selected item. +---@return core.contextmenu.item|nil function ContextMenu:get_item_selected() return (self.items or {})[self.selected] end +---Hides the context menu and performs the command if an item is selected. function ContextMenu:call_selected_item() local selected = self:get_item_selected() self:hide() @@ -166,6 +207,12 @@ function ContextMenu:call_selected_item() end end +---Event handler for mouse press. +---@param button core.view.mousebutton +---@param px number +---@param py number +---@param clicks number +---@return boolean # true if the event is caught. function ContextMenu:on_mouse_pressed(button, px, py, clicks) local caught = false @@ -186,14 +233,20 @@ function ContextMenu:on_mouse_pressed(button, px, py, clicks) return caught end +---@type fun(self: table, k: string, dest: number, rate?: number, name?: string) ContextMenu.move_towards = View.move_towards +---Event handler for content update. function ContextMenu:update() if self.show_context_menu then self:move_towards("height", self.items.height, nil, "contextmenu") end end +---Draws the context menu. +--- +---This wraps `ContextMenu:draw_context_menu()`. +---@see core.contextmenu.draw_context_menu function ContextMenu:draw() if not self.show_context_menu then return end if self.current_scale ~= SCALE then @@ -206,6 +259,7 @@ function ContextMenu:draw() core.root_view:defer_draw(self.draw_context_menu, self) end +---Draws the context menu. function ContextMenu:draw_context_menu() if not self.items then return end local bx, by, bw, bh = self.position.x, self.position.y, self.items.width, self.height diff --git a/data/core/dirwatch.lua b/data/core/dirwatch.lua index e3b6d61c..d9ac4523 100644 --- a/data/core/dirwatch.lua +++ b/data/core/dirwatch.lua @@ -91,6 +91,7 @@ end -- designed to be run inside a coroutine. function dirwatch:check(change_callback, scan_time, wait_time) local had_change = false + local last_error self.monitor:check(function(id) had_change = true if self.monitor:mode() == "single" then @@ -102,7 +103,10 @@ function dirwatch:check(change_callback, scan_time, wait_time) elseif self.reverse_watched[id] then change_callback(self.reverse_watched[id]) end + end, function(err) + last_error = err end) + if last_error ~= nil then error(last_error) end local start_time = system.get_time() for directory, old_modified in pairs(self.scanned) do if old_modified then @@ -186,49 +190,47 @@ end -- "root" will by an absolute path without trailing '/' -- "path" will be a path starting without '/' and without trailing '/' -- or the empty string. --- It will identifies a sub-path within "root. +-- It identifies a sub-path within "root". -- The current path location will therefore always be: root .. path. --- When recursing "root" will always be the same, only "path" will change. +-- When recursing, "root" will always be the same, only "path" will change. -- Returns a list of file "items". In each item the "filename" will be the -- complete file path relative to "root" *without* the trailing '/', and without the starting '/'. -function dirwatch.get_directory_files(dir, root, path, t, entries_count, recurse_pred) +function dirwatch.get_directory_files(dir, root, path, entries_count, recurse_pred) + local t = {} local t0 = system.get_time() - local t_elapsed = system.get_time() - t0 - local dirs, files = {}, {} local ignore_compiled = compile_ignore_files() - local all = system.list_dir(root .. PATHSEP .. path) if not all then return nil end - - for _, file in ipairs(all or {}) do + local entries = { } + for _, file in ipairs(all) do local info = get_project_file_info(root, (path ~= "" and (path .. PATHSEP) or "") .. file, ignore_compiled) if info then - table.insert(info.type == "dir" and dirs or files, info) - entries_count = entries_count + 1 + table.insert(entries, info) end end + table.sort(entries, compare_file) local recurse_complete = true - table.sort(dirs, compare_file) - for _, f in ipairs(dirs) do - table.insert(t, f) - if recurse_pred(dir, f.filename, entries_count, t_elapsed) then - local _, complete, n = dirwatch.get_directory_files(dir, root, f.filename, t, entries_count, recurse_pred) - recurse_complete = recurse_complete and complete - if n ~= nil then - entries_count = n + for _, info in ipairs(entries) do + table.insert(t, info) + entries_count = entries_count + 1 + if info.type == "dir" then + if recurse_pred(dir, info.filename, entries_count, system.get_time() - t0) then + local t_rec, complete, n = dirwatch.get_directory_files(dir, root, info.filename, entries_count, recurse_pred) + recurse_complete = recurse_complete and complete + if n ~= nil then + entries_count = n + for _, info_rec in ipairs(t_rec) do + table.insert(t, info_rec) + end + end + else + recurse_complete = false end - else - recurse_complete = false end end - table.sort(files, compare_file) - for _, f in ipairs(files) do - table.insert(t, f) - end - return t, recurse_complete, entries_count end diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index 086e9f3e..e44610e5 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -1,5 +1,6 @@ local Object = require "core.object" local Highlighter = require "core.doc.highlighter" +local translate = require "core.doc.translate" local core = require "core" local syntax = require "core.syntax" local config = require "core.config" @@ -27,9 +28,11 @@ function Doc:new(filename, abs_filename, new_file) self:load(filename) end end + if new_file then + self.crlf = config.line_endings == "crlf" + end end - function Doc:reset() self.lines = { "\n" } self.selections = { 1, 1, 1, 1 } @@ -38,10 +41,10 @@ function Doc:reset() self.redo_stack = { idx = 1 } self.clean_change_id = 1 self.highlighter = Highlighter(self) + self.overwrite = false self:reset_syntax() end - function Doc:reset_syntax() local header = self:get_text(1, 1, self:position_offset(1, 1, 128)) local path = self.abs_filename @@ -56,16 +59,14 @@ function Doc:reset_syntax() end end - function Doc:set_filename(filename, abs_filename) self.filename = filename self.abs_filename = abs_filename self:reset_syntax() end - function Doc:load(filename) - local fp = assert( io.open(filename, "rb") ) + local fp = assert(io.open(filename, "rb")) self:reset() self.lines = {} local i = 1 @@ -85,7 +86,6 @@ function Doc:load(filename) self:reset_syntax() end - function Doc:reload() if self.filename then local sel = { self:get_selection() } @@ -95,7 +95,6 @@ function Doc:reload() end end - function Doc:save(filename, abs_filename) if not filename then assert(self.filename, "no filename set to default to") @@ -104,7 +103,7 @@ function Doc:save(filename, abs_filename) else assert(self.filename or abs_filename, "calling save on unnamed doc without absolute path") end - local fp = assert( io.open(filename, "wb") ) + local fp = assert(io.open(filename, "wb")) for _, line in ipairs(self.lines) do if self.crlf then line = line:gsub("\n", "\r\n") end fp:write(line) @@ -115,34 +114,30 @@ function Doc:save(filename, abs_filename) self:clean() end - function Doc:get_name() return self.filename or "unsaved" end - function Doc:is_dirty() if self.new_file then + if self.filename then return true end return #self.lines > 1 or #self.lines[1] > 1 else return self.clean_change_id ~= self:get_change_id() end end - function Doc:clean() self.clean_change_id = self:get_change_id() end - function Doc:get_indent_info() if not self.indent_info then return config.tab_type, config.indent_size, false end return self.indent_info.type or config.tab_type, - self.indent_info.size or config.indent_size, - self.indent_info.confirmed + self.indent_info.size or config.indent_size, + self.indent_info.confirmed end - function Doc:get_change_id() return self.undo_stack.idx end @@ -166,13 +161,14 @@ function Doc:get_selection(sort) return line1, col1, line2, col2, swap end - ---Get the selection specified by `idx` ---@param idx integer @the index of the selection to retrieve ---@param sort? boolean @whether to sort the selection returned ---@return integer,integer,integer,integer,boolean? @line1, col1, line2, col2, was the selection sorted function Doc:get_selection_idx(idx, sort) - local line1, col1, line2, col2 = self.selections[idx*4-3], self.selections[idx*4-2], self.selections[idx*4-1], self.selections[idx*4] + local line1, col1, line2, col2 = self.selections[idx * 4 - 3], self.selections[idx * 4 - 2], + self.selections[idx * 4 - 1], + self.selections[idx * 4] if line1 and sort then return sort_positions(line1, col1, line2, col2) else @@ -216,7 +212,7 @@ function Doc:set_selections(idx, line1, col1, line2, col2, swap, rm) if swap then line1, col1, line2, col2 = line2, col2, line1, col1 end line1, col1 = self:sanitize_position(line1, col1) line2, col2 = self:sanitize_position(line2 or line1, col2 or col1) - common.splice(self.selections, (idx - 1)*4 + 1, rm == nil and 4 or rm, { line1, col1, line2, col2 }) + common.splice(self.selections, (idx - 1) * 4 + 1, rm == nil and 4 or rm, { line1, col1, line2, col2 }) end function Doc:add_selection(line1, col1, line2, col2, swap) @@ -232,7 +228,6 @@ function Doc:add_selection(line1, col1, line2, col2, swap) self.last_selection = target end - function Doc:remove_selection(idx) if self.last_selection >= idx then self.last_selection = self.last_selection - 1 @@ -240,7 +235,6 @@ function Doc:remove_selection(idx) common.splice(self.selections, (idx - 1) * 4 + 1, 4) end - function Doc:set_selection(line1, col1, line2, col2, swap) self.selections = {} self:set_selections(1, line1, col1, line2, col2, swap) @@ -251,24 +245,24 @@ function Doc:merge_cursors(idx) for i = (idx or (#self.selections - 3)), (idx or 5), -4 do for j = 1, i - 4, 4 do if self.selections[i] == self.selections[j] and - self.selections[i+1] == self.selections[j+1] then - common.splice(self.selections, i, 4) - if self.last_selection >= (i+3)/4 then - self.last_selection = self.last_selection - 1 - end - break + 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 end end local function selection_iterator(invariant, idx) - local target = invariant[3] and (idx*4 - 7) or (idx*4 + 1) + local target = invariant[3] and (idx * 4 - 7) or (idx * 4 + 1) if target > #invariant[1] or target <= 0 or (type(invariant[3]) == "number" and invariant[3] ~= idx - 1) then return end if invariant[2] then - return idx+(invariant[3] and -1 or 1), sort_positions(table.unpack(invariant[1], target, target+4)) + return idx + (invariant[3] and -1 or 1), sort_positions(table.unpack(invariant[1], target, target + 4)) else - return idx+(invariant[3] and -1 or 1), table.unpack(invariant[1], target, target+4) + return idx + (invariant[3] and -1 or 1), table.unpack(invariant[1], target, target + 4) end end @@ -276,8 +270,9 @@ end -- If a number, runs for exactly that iteration. function Doc:get_selections(sort_intra, idx_reverse) return selection_iterator, { self.selections, sort_intra, idx_reverse }, - idx_reverse == true and ((#self.selections / 4) + 1) or ((idx_reverse or -1)+1) + idx_reverse == true and ((#self.selections / 4) + 1) or ((idx_reverse or -1) + 1) end + -- End of cursor seciton. function Doc:sanitize_position(line, col) @@ -290,7 +285,6 @@ function Doc:sanitize_position(line, col) return line, common.clamp(col, 1, #self.lines[line]) end - local function position_offset_func(self, line, col, fn, ...) line, col = self:sanitize_position(line, col) return fn(self, line, col, ...) @@ -329,7 +323,6 @@ function Doc:position_offset(line, col, ...) end end - function Doc:get_text(line1, col1, line2, col2) line1, col1 = self:sanitize_position(line1, col1) line2, col2 = self:sanitize_position(line2, col2) @@ -345,13 +338,11 @@ function Doc:get_text(line1, col1, line2, col2) return table.concat(lines) end - function Doc:get_char(line, col) line, col = self:sanitize_position(line, col) return self.lines[line]:sub(col, col) end - local function push_undo(undo_stack, time, type, ...) undo_stack[undo_stack.idx] = { type = type, time = time, ... } undo_stack[undo_stack.idx - config.max_undos] = nil @@ -412,7 +403,8 @@ function Doc:raw_insert(line, col, text, undo_stack, time) if cline1 < line then break end local line_addition = (line < cline1 or col < ccol1) and #lines - 1 or 0 local column_addition = line == cline1 and ccol1 > col and len or 0 - self:set_selections(idx, cline1 + line_addition, ccol1 + column_addition, cline2 + line_addition, ccol2 + column_addition) + self:set_selections(idx, cline1 + line_addition, ccol1 + column_addition, cline2 + line_addition, + ccol2 + column_addition) end -- push undo @@ -425,7 +417,6 @@ function Doc:raw_insert(line, col, text, undo_stack, time) self:sanitize_selection() end - function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time) -- push undo local text = self:get_text(line1, col1, line2, col2) @@ -484,15 +475,17 @@ function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time) self:sanitize_selection() end - function Doc:insert(line, col, text) self.redo_stack = { idx = 1 } + -- Reset the clean id when we're pushing something new before it + if self:get_change_id() < self.clean_change_id then + self.clean_change_id = -1 + end line, col = self:sanitize_position(line, col) self:raw_insert(line, col, text, self.undo_stack, system.get_time()) self:on_text_change("insert") end - function Doc:remove(line1, col1, line2, col2) self.redo_stack = { idx = 1 } line1, col1 = self:sanitize_position(line1, col1) @@ -502,28 +495,34 @@ function Doc:remove(line1, col1, line2, col2) self:on_text_change("remove") end - function Doc:undo() pop_undo(self, self.undo_stack, self.redo_stack, false) end - function Doc:redo() pop_undo(self, self.redo_stack, self.undo_stack, false) end - function Doc:text_input(text, idx) for sidx, line1, col1, line2, col2 in self:get_selections(true, idx or true) do + local had_selection = false if line1 ~= line2 or col1 ~= col2 then self:delete_to_cursor(sidx) + had_selection = true end + + if self.overwrite + and not had_selection + and col1 < #self.lines[line1] + and text:ulen() == 1 then + self:remove(line1, col1, translate.next_char(self, line1, col1)) + end + self:insert(line1, col1, text) self:move_to_cursor(sidx, #text) end end - function Doc:ime_text_editing(text, start, length, idx) for sidx, line1, col1, line2, col2 in self:get_selections(true, idx or true) do if line1 ~= line2 or col1 ~= col2 then @@ -534,7 +533,6 @@ function Doc:ime_text_editing(text, start, length, idx) end end - function Doc:replace_cursor(idx, line1, col1, line2, col2, fn) local old_text = self:get_text(line1, col1, line2, col2) local new_text, res = fn(old_text) @@ -550,7 +548,7 @@ function Doc:replace_cursor(idx, line1, col1, line2, col2, fn) end function Doc:replace(fn) - local has_selection, results = false, { } + local has_selection, results = false, {} for idx, line1, col1, line2, col2 in self:get_selections(true) do if line1 ~= line2 or col1 ~= col2 then results[idx] = self:replace_cursor(idx, line1, col1, line2, col2, fn) @@ -564,7 +562,6 @@ function Doc:replace(fn) return results end - function Doc:delete_to_cursor(idx, ...) for sidx, line1, col1, line2, col2 in self:get_selections(true, idx) do if line1 ~= line2 or col1 ~= col2 then @@ -578,6 +575,7 @@ function Doc:delete_to_cursor(idx, ...) end self:merge_cursors(idx) end + function Doc:delete_to(...) return self:delete_to_cursor(nil, ...) end function Doc:move_to_cursor(idx, ...) @@ -586,8 +584,8 @@ function Doc:move_to_cursor(idx, ...) end self:merge_cursors(idx) end -function Doc:move_to(...) return self:move_to_cursor(nil, ...) end +function Doc:move_to(...) return self:move_to_cursor(nil, ...) end function Doc:select_to_cursor(idx, ...) for sidx, line, col, line2, col2 in self:get_selections(false, idx) do @@ -596,8 +594,8 @@ function Doc:select_to_cursor(idx, ...) end self:merge_cursors(idx) end -function Doc:select_to(...) return self:select_to_cursor(nil, ...) end +function Doc:select_to(...) return self:select_to_cursor(nil, ...) end function Doc:get_indent_string() local indent_type, indent_size = self:get_indent_info() @@ -620,7 +618,7 @@ function Doc:get_line_indent(line, rnd_up) local indent = e and line:sub(1, e):gsub("\t", soft_tab) or "" local number = #indent / #soft_tab return e, indent:sub(1, - (rnd_up and math.ceil(number) or math.floor(number))*#soft_tab) + (rnd_up and math.ceil(number) or math.floor(number)) * #soft_tab) end end @@ -669,5 +667,4 @@ function Doc:on_close() core.log_quiet("Closed doc \"%s\"", self:get_name()) end - return Doc diff --git a/data/core/doc/search.lua b/data/core/doc/search.lua index 8395769a..3cfe6a2d 100644 --- a/data/core/doc/search.lua +++ b/data/core/doc/search.lua @@ -66,7 +66,18 @@ function search.find(doc, line, col, text, opt) s, e = search_func(line_text, pattern, col, plain) end if s then - return line, s, line, e + 1 + local line2 = line + -- If we've matched the newline too, + -- return until the initial character of the next line. + if e >= #doc.lines[line] then + line2 = line + 1 + e = 0 + end + -- Avoid returning matches that go beyond the last line. + -- This is needed to avoid selecting the "last" newline. + if line2 <= #doc.lines then + return line, s, line2, e + 1 + end end col = opt.reverse and -1 or 1 end diff --git a/data/core/docview.lua b/data/core/docview.lua index a7e4aed8..698f2169 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -460,9 +460,16 @@ function DocView:draw_line_text(line, x, y) return self:get_line_height() end + +function DocView:draw_overwrite_caret(x, y, width) + local lh = self:get_line_height() + renderer.draw_rect(x, y + lh - style.caret_width, width, style.caret_width, style.caret) +end + + function DocView:draw_caret(x, y) - local lh = self:get_line_height() - renderer.draw_rect(x, y, style.caret_width, lh, style.caret) + local lh = self:get_line_height() + renderer.draw_rect(x, y, style.caret_width, lh, style.caret) end function DocView:draw_line_body(line, x, y) @@ -559,7 +566,12 @@ function DocView:draw_overlay() else if config.disable_blink or (core.blink_timer - core.blink_start) % T < T / 2 then - self:draw_caret(self:get_line_screen_position(line1, col1)) + local x, y = self:get_line_screen_position(line1, col1) + if self.doc.overwrite then + self:draw_overwrite_caret(x, y, self:get_font():get_width(self.doc:get_char(line1, col1))) + else + self:draw_caret(x, y) + end end end end diff --git a/data/core/init.lua b/data/core/init.lua index 316321c2..db1e10a3 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -102,7 +102,7 @@ local function strip_leading_path(filename) end local function strip_trailing_slash(filename) - if filename:match("[^:][/\\]$") then + if filename:match("[^:]["..PATHSEP.."]$") then return filename:sub(1, -2) end return filename @@ -120,9 +120,7 @@ local function show_max_files_warning(dir) "Too many files in project directory: stopped reading at ".. config.max_project_files.." files. For more information see ".. "usage.md at https://github.com/lite-xl/lite-xl." - if core.status_view then - core.status_view:show_message("!", style.accent, message) - end + core.warn(message) end @@ -184,7 +182,7 @@ local function refresh_directory(topdir, target) directory_start_idx = directory_start_idx + 1 end - local files = dirwatch.get_directory_files(topdir, topdir.name, (target or ""), {}, 0, function() return false end) + local files = dirwatch.get_directory_files(topdir, topdir.name, (target or ""), 0, function() return false end) local change = false -- If this file doesn't exist, we should be calling this on our parent directory, assume we'll do that. @@ -265,7 +263,7 @@ function core.add_project_directory(path) local fstype = PLATFORM == "Linux" and system.get_fs_type(topdir.name) or "unknown" topdir.force_scans = (fstype == "nfs" or fstype == "fuse") - local t, complete, entries_count = dirwatch.get_directory_files(topdir, topdir.name, "", {}, 0, timed_max_files_pred) + local t, complete, entries_count = dirwatch.get_directory_files(topdir, topdir.name, "", 0, timed_max_files_pred) topdir.files = t if not complete then topdir.slow_filesystem = not complete and (entries_count <= config.max_project_files) @@ -810,7 +808,11 @@ function core.init() end if not plugins_success or got_user_error or got_project_error then - command.perform("core:open-log") + -- defer LogView to after everything is initialized, + -- so that EmptyView won't be added after LogView. + core.add_thread(function() + command.perform("core:open-log") + end) end core.configure_borderless_window() @@ -1274,6 +1276,9 @@ function core.on_event(type, ...) elseif type == "textediting" then ime.on_text_editing(...) elseif type == "keypressed" then + -- In some cases during IME composition input is still sent to us + -- so we just ignore it. + if ime.editing then return false end did_keymap = keymap.on_key_pressed(...) elseif type == "keyreleased" then keymap.on_key_released(...) @@ -1418,11 +1423,11 @@ local run_threads = coroutine.wrap(function() -- stop running threads if we're about to hit the end of frame if system.get_time() - core.frame_start > max_time then - coroutine.yield(0) + coroutine.yield(0, false) end end - coroutine.yield(minimal_time_to_wake) + coroutine.yield(minimal_time_to_wake, true) end end) @@ -1430,10 +1435,15 @@ end) function core.run() local next_step local last_frame_time + local run_threads_full = 0 while true do core.frame_start = system.get_time() - local time_to_wake = run_threads() + local time_to_wake, threads_done = run_threads() + if threads_done then + run_threads_full = run_threads_full + 1 + end local did_redraw = false + local did_step = false local force_draw = core.redraw and last_frame_time and core.frame_start - last_frame_time > (1 / config.fps) if force_draw or not next_step or system.get_time() >= next_step then if core.step() then @@ -1441,11 +1451,12 @@ function core.run() last_frame_time = core.frame_start end next_step = nil + did_step = true end if core.restart_request or core.quit_request then break end if not did_redraw then - if system.window_has_focus() then + if system.window_has_focus() or not did_step or run_threads_full < 2 then local now = system.get_time() if not next_step then -- compute the time until the next blink local t = now - core.blink_start @@ -1454,7 +1465,7 @@ function core.run() local cursor_time_to_wake = dt + 1 / config.fps next_step = now + cursor_time_to_wake end - if time_to_wake > 0 and system.wait_event(math.min(next_step - now, time_to_wake)) then + if system.wait_event(math.min(next_step - now, time_to_wake)) then next_step = nil -- if we've recevied an event, perform a step end else @@ -1462,6 +1473,7 @@ function core.run() next_step = nil -- perform a step when we're not in focus if get we an event end else -- if we redrew, then make sure we only draw at most FPS/sec + run_threads_full = 0 local now = system.get_time() local elapsed = now - core.frame_start local next_frame = math.max(0, 1 / config.fps - elapsed) diff --git a/data/core/keymap.lua b/data/core/keymap.lua index 0f30078b..02636bd3 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -42,15 +42,19 @@ local modkeys = modkeys_os.keys ---@return string local function normalize_stroke(stroke) local stroke_table = {} - for modkey in stroke:gmatch("(%w+)%+") do - table.insert(stroke_table, modkey) + for key in stroke:gmatch("[^+]+") do + table.insert(stroke_table, key) end - if not next(stroke_table) then - return stroke - end - table.sort(stroke_table) - local new_stroke = table.concat(stroke_table, "+") .. "+" - return new_stroke .. stroke:sub(new_stroke:len() + 1) + table.sort(stroke_table, function(a, b) + if a == b then return false end + for _, mod in ipairs(modkeys) do + if a == mod or b == mod then + return a == mod + end + end + return a < b + end) + return table.concat(stroke_table, "+") end @@ -58,13 +62,13 @@ end ---@param key string ---@return string local function key_to_stroke(key) - local stroke = "" + local keys = { key } for _, mk in ipairs(modkeys) do if keymap.modkeys[mk] then - stroke = stroke .. mk .. "+" + table.insert(keys, mk) end end - return normalize_stroke(stroke) .. key + return normalize_stroke(table.concat(keys, "+")) end ---Remove the given value from an array associated to a key in a table. @@ -92,12 +96,12 @@ end ---@param map keymap.map local function remove_duplicates(map) for stroke, commands in pairs(map) do - stroke = normalize_stroke(stroke) + local normalized_stroke = normalize_stroke(stroke) if type(commands) == "string" or type(commands) == "function" then commands = { commands } end - if keymap.map[stroke] then - for _, registered_cmd in ipairs(keymap.map[stroke]) do + if keymap.map[normalized_stroke] then + for _, registered_cmd in ipairs(keymap.map[normalized_stroke]) do local j = 0 for i=1, #commands do while commands[i + j] == registered_cmd do @@ -120,7 +124,6 @@ end function keymap.add_direct(map) for stroke, commands in pairs(map) do stroke = normalize_stroke(stroke) - if type(commands) == "string" or type(commands) == "function" then commands = { commands } end @@ -174,7 +177,8 @@ end ---@param shortcut string ---@param cmd string function keymap.unbind(shortcut, cmd) - remove_only(keymap.map, normalize_stroke(shortcut), cmd) + shortcut = normalize_stroke(shortcut) + remove_only(keymap.map, shortcut, cmd) remove_only(keymap.reverse_map, cmd, shortcut) end @@ -199,10 +203,6 @@ end -- Events listening -------------------------------------------------------------------------------- function keymap.on_key_pressed(k, ...) - -- In MacOS and Windows during IME composition input is still sent to us - -- so we just ignore it - if PLATFORM ~= "Linux" and ime.editing then return false end - local mk = modkey_map[k] if mk then keymap.modkeys[mk] = true @@ -341,6 +341,7 @@ keymap.add_direct { ["ctrl+x"] = "doc:cut", ["ctrl+c"] = "doc:copy", ["ctrl+v"] = "doc:paste", + ["insert"] = "doc:toggle-overwrite", ["ctrl+insert"] = "doc:copy", ["shift+insert"] = "doc:paste", ["escape"] = { "command:escape", "doc:select-none", "dialog:select-no" }, diff --git a/data/core/modkeys-generic.lua b/data/core/modkeys-generic.lua index 6e264061..613813dc 100644 --- a/data/core/modkeys-generic.lua +++ b/data/core/modkeys-generic.lua @@ -7,8 +7,12 @@ modkeys.map = { ["right shift"] = "shift", ["left alt"] = "alt", ["right alt"] = "altgr", + ["left gui"] = "super", + ["left windows"] = "super", + ["right gui"] = "super", + ["right windows"] = "super" } -modkeys.keys = { "ctrl", "alt", "altgr", "shift" } +modkeys.keys = { "ctrl", "shift", "alt", "altgr", "super" } return modkeys diff --git a/data/core/modkeys-macos.lua b/data/core/modkeys-macos.lua index e9fd0665..86b28de0 100644 --- a/data/core/modkeys-macos.lua +++ b/data/core/modkeys-macos.lua @@ -13,6 +13,6 @@ modkeys.map = { ["right alt"] = "altgr", } -modkeys.keys = { "cmd", "ctrl", "alt", "option", "altgr", "shift" } +modkeys.keys = { "ctrl", "alt", "option", "altgr", "shift", "cmd" } return modkeys diff --git a/data/core/nagview.lua b/data/core/nagview.lua index b66da75c..1a7fa193 100644 --- a/data/core/nagview.lua +++ b/data/core/nagview.lua @@ -24,6 +24,7 @@ function NagView:new() self.scrollable = true self.target_height = 0 self.on_mouse_pressed_root = nil + self.dim_alpha = 0 end function NagView:get_title() @@ -68,7 +69,9 @@ function NagView:dim_window_content() oy = oy + self.show_height local w, h = core.root_view.size.x, core.root_view.size.y - oy core.root_view:defer_draw(function() - renderer.draw_rect(ox, oy, w, h, style.nagbar_dim) + local dim_color = { table.unpack(style.nagbar_dim) } + dim_color[4] = style.nagbar_dim[4] * self.dim_alpha + renderer.draw_rect(ox, oy, w, h, dim_color) end) end @@ -172,10 +175,13 @@ function NagView:update() NagView.super.update(self) if self.visible and core.active_view == self and self.title then - self:move_towards(self, "show_height", self:get_target_height(), nil, "nagbar") + local target_height = self:get_target_height() + self:move_towards(self, "show_height", target_height, nil, "nagbar") self:move_towards(self, "underline_progress", 1, nil, "nagbar") + self:move_towards(self, "dim_alpha", self.show_height / target_height, nil, "nagbar") else self:move_towards(self, "show_height", 0, nil, "nagbar") + self:move_towards(self, "dim_alpha", 0, nil, "nagbar") if self.show_height <= 0 then self.title = nil self.message = nil diff --git a/data/core/node.lua b/data/core/node.lua index 3ae9b256..86d4e4e0 100644 --- a/data/core/node.lua +++ b/data/core/node.lua @@ -177,8 +177,12 @@ function Node:add_view(view, idx) assert(not self.locked, "Tried to add view to locked node") if self.views[1] and self.views[1]:is(EmptyView) then table.remove(self.views) + if idx and idx > 1 then + idx = idx - 1 + end end - table.insert(self.views, idx or (#self.views + 1), view) + idx = common.clamp(idx or (#self.views + 1), 1, (#self.views + 1)) + table.insert(self.views, idx, view) self:set_active_view(view) end diff --git a/data/core/rootview.lua b/data/core/rootview.lua index 18ae40ab..ec9d5e0e 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -313,10 +313,10 @@ function RootView:on_mouse_moved(x, y, dx, dy) if self.dragged_divider then local node = self.dragged_divider if node.type == "hsplit" then - x = common.clamp(x, 0, self.root_node.size.x * 0.95) + x = common.clamp(x - node.position.x, 0, self.root_node.size.x * 0.95) resize_child_node(node, "x", x, dx) elseif node.type == "vsplit" then - y = common.clamp(y, 0, self.root_node.size.y * 0.95) + y = common.clamp(y - node.position.y, 0, self.root_node.size.y * 0.95) resize_child_node(node, "y", y, dy) end node.divider = common.clamp(node.divider, 0.01, 0.99) @@ -406,10 +406,10 @@ function RootView:on_touch_moved(x, y, dx, dy, ...) if self.dragged_divider then local node = self.dragged_divider if node.type == "hsplit" then - x = common.clamp(x, 0, self.root_node.size.x * 0.95) + x = common.clamp(x - node.position.x, 0, self.root_node.size.x * 0.95) resize_child_node(node, "x", x, dx) elseif node.type == "vsplit" then - y = common.clamp(y, 0, self.root_node.size.y * 0.95) + y = common.clamp(y - node.position.y, 0, self.root_node.size.y * 0.95) resize_child_node(node, "y", y, dy) end node.divider = common.clamp(node.divider, 0.01, 0.99) diff --git a/data/core/scrollbar.lua b/data/core/scrollbar.lua index d2bb0562..29352dd4 100644 --- a/data/core/scrollbar.lua +++ b/data/core/scrollbar.lua @@ -58,9 +58,9 @@ function Scrollbar:new(options) ---@type "expanded" | "contracted" | false @Force the scrollbar status self.force_status = options.force_status self:set_forced_status(options.force_status) - ---@type number? @Override the default value specified by `style.expanded_scrollbar_size` - self.contracted_size = options.contracted_size ---@type number? @Override the default value specified by `style.scrollbar_size` + self.contracted_size = options.contracted_size + ---@type number? @Override the default value specified by `style.expanded_scrollbar_size` self.expanded_size = options.expanded_size end @@ -121,7 +121,7 @@ function Scrollbar:_get_thumb_rect_normal() across_size = across_size + (expanded_scrollbar_size - scrollbar_size) * self.expand_percent return nr.across + nr.across_size - across_size, - nr.along + self.percent * nr.scrollable * (nr.along_size - along_size) / (sz - nr.along_size), + nr.along + self.percent * (nr.along_size - along_size), across_size, along_size end @@ -189,8 +189,9 @@ function Scrollbar:_on_mouse_pressed_normal(button, x, y, clicks) self.drag_start_offset = along - y return true elseif overlaps == "track" then + local nr = self.normal_rect self.drag_start_offset = - along_size / 2 - return (y - self.normal_rect.along - along_size / 2) / self.normal_rect.along_size + return common.clamp((y - nr.along - along_size / 2) / (nr.along_size - along_size), 0, 1) end end end @@ -237,7 +238,8 @@ end function Scrollbar:_on_mouse_moved_normal(x, y, dx, dy) if self.dragging then local nr = self.normal_rect - return common.clamp((y - nr.along + self.drag_start_offset) / nr.along_size, 0, 1) + local _, _, _, along_size = self:_get_thumb_rect_normal() + return common.clamp((y - nr.along + self.drag_start_offset) / (nr.along_size - along_size), 0, 1) end return self:_update_hover_status_normal(x, y) end @@ -280,7 +282,7 @@ function Scrollbar:set_size(x, y, w, h, scrollable) end ---Updates the scrollbar location ----@param percent number @number between 0 and 1 representing the position of the middle part of the thumb +---@param percent number @number between 0 and 1 where 0 means thumb at the top and 1 at the bottom function Scrollbar:set_percent(percent) self.percent = percent end diff --git a/data/core/start.lua b/data/core/start.lua index 2af131fc..746ae00c 100644 --- a/data/core/start.lua +++ b/data/core/start.lua @@ -1,11 +1,11 @@ -- this file is used by lite-xl to setup the Lua environment when starting -VERSION = "2.1.1r3" +VERSION = "2.1.2r1" MOD_VERSION_MAJOR = 3 MOD_VERSION_MINOR = 0 MOD_VERSION_PATCH = 0 MOD_VERSION_STRING = string.format("%d.%d.%d", MOD_VERSION_MAJOR, MOD_VERSION_MINOR, MOD_VERSION_PATCH) -SCALE = tonumber(os.getenv("LITE_SCALE") or os.getenv("GDK_SCALE") or os.getenv("QT_SCALE_FACTOR")) or SCALE +SCALE = tonumber(os.getenv("LITE_SCALE") or os.getenv("GDK_SCALE") or os.getenv("QT_SCALE_FACTOR")) or 1 PATHSEP = package.config:sub(1, 1) EXEDIR = EXEFILE:match("^(.+)[/\\][^/\\]+$") diff --git a/data/core/statusview.lua b/data/core/statusview.lua index 731c0552..74124768 100644 --- a/data/core/statusview.lua +++ b/data/core/statusview.lua @@ -232,15 +232,27 @@ function StatusView:register_docview_items() return { style.text, line, ":", col > config.line_limit and style.accent or style.text, col, - style.text, - self.separator, - string.format("%.f%%", line / #dv.doc.lines * 100) + style.text } end, command = "doc:go-to-line", tooltip = "line : column" }) + self:add_item({ + predicate = predicate_docview, + name = "doc:position-percent", + alignment = StatusView.Item.LEFT, + get_item = function() + local dv = core.active_view + local line = dv.doc:get_selection() + return { + string.format("%.f%%", line / #dv.doc.lines * 100) + } + end, + tooltip = "caret position" + }) + self:add_item({ predicate = predicate_docview, name = "doc:selections", @@ -307,6 +319,19 @@ function StatusView:register_docview_items() end, command = "doc:toggle-line-ending" }) + + self:add_item { + predicate = predicate_docview, + name = "doc:overwrite-mode", + alignment = StatusView.Item.RIGHT, + get_item = function() + return { + style.text, core.active_view.doc.overwrite and "OVR" or "INS" + } + end, + command = "doc:toggle-overwrite", + separator = StatusView.separator2 + } end diff --git a/data/core/syntax.lua b/data/core/syntax.lua index 9862fbdf..6555a17c 100644 --- a/data/core/syntax.lua +++ b/data/core/syntax.lua @@ -3,7 +3,7 @@ local common = require "core.common" local syntax = {} syntax.items = {} -local plain_text_syntax = { name = "Plain Text", patterns = {}, symbols = {} } +syntax.plain_text_syntax = { name = "Plain Text", patterns = {}, symbols = {} } function syntax.add(t) @@ -46,7 +46,7 @@ end function syntax.get(filename, header) return (filename and find(filename, "files")) or (header and find(header, "headers")) - or plain_text_syntax + or syntax.plain_text_syntax end diff --git a/data/core/tokenizer.lua b/data/core/tokenizer.lua index 6052b449..a027712f 100644 --- a/data/core/tokenizer.lua +++ b/data/core/tokenizer.lua @@ -210,9 +210,11 @@ function tokenizer.tokenize(incoming_syntax, text, state, resume) -- Remove '^' from the beginning of the pattern if type(target) == "table" then target[p_idx] = code:usub(2) + code = target[p_idx] else p.pattern = p.pattern and code:usub(2) p.regex = p.regex and code:usub(2) + code = p.pattern or p.regex end end end diff --git a/data/core/view.lua b/data/core/view.lua index 02560ff8..bab75b01 100644 --- a/data/core/view.lua +++ b/data/core/view.lua @@ -142,14 +142,14 @@ function View:on_mouse_pressed(button, x, y, clicks) local result = self.v_scrollbar:on_mouse_pressed(button, x, y, clicks) if result then if result ~= true then - self.scroll.to.y = result * self:get_scrollable_size() + self.scroll.to.y = result * (self:get_scrollable_size() - self.size.y) end return true end result = self.h_scrollbar:on_mouse_pressed(button, x, y, clicks) if result then if result ~= true then - self.scroll.to.x = result * self:get_h_scrollable_size() + self.scroll.to.x = result * (self:get_h_scrollable_size() - self.size.x) end return true end @@ -177,7 +177,7 @@ function View:on_mouse_moved(x, y, dx, dy) result = self.v_scrollbar:on_mouse_moved(x, y, dx, dy) if result then if result ~= true then - self.scroll.to.y = result * self:get_scrollable_size() + self.scroll.to.y = result * (self:get_scrollable_size() - self.size.y) if not config.animate_drag_scroll then self:clamp_scroll_position() self.scroll.y = self.scroll.to.y @@ -191,7 +191,7 @@ function View:on_mouse_moved(x, y, dx, dy) result = self.h_scrollbar:on_mouse_moved(x, y, dx, dy) if result then if result ~= true then - self.scroll.to.x = result * self:get_h_scrollable_size() + self.scroll.to.x = result * (self:get_h_scrollable_size() - self.size.x) if not config.animate_drag_scroll then self:clamp_scroll_position() self.scroll.x = self.scroll.to.x @@ -287,12 +287,16 @@ end function View:update_scrollbar() local v_scrollable = self:get_scrollable_size() self.v_scrollbar:set_size(self.position.x, self.position.y, self.size.x, self.size.y, v_scrollable) - self.v_scrollbar:set_percent(self.scroll.y/v_scrollable) + local v_percent = self.scroll.y/(v_scrollable - self.size.y) + -- Avoid setting nan percent + self.v_scrollbar:set_percent(v_percent == v_percent and v_percent or 0) self.v_scrollbar:update() local h_scrollable = self:get_h_scrollable_size() self.h_scrollbar:set_size(self.position.x, self.position.y, self.size.x, self.size.y, h_scrollable) - self.h_scrollbar:set_percent(self.scroll.x/h_scrollable) + local h_percent = self.scroll.x/(h_scrollable - self.size.x) + -- Avoid setting nan percent + self.h_scrollbar:set_percent(h_percent == h_percent and h_percent or 0) self.h_scrollbar:update() end diff --git a/data/plugins/autocomplete.lua b/data/plugins/autocomplete.lua index cf228b6e..98b2dcd0 100644 --- a/data/plugins/autocomplete.lua +++ b/data/plugins/autocomplete.lua @@ -10,6 +10,10 @@ local RootView = require "core.rootview" local DocView = require "core.docview" local Doc = require "core.doc" +---Symbols cache of all open documents +---@type table +local cache = setmetatable({}, { __mode = "k" }) + config.plugins.autocomplete = common.merge({ -- Amount of characters that need to be written for autocomplete min_len = 3, @@ -19,8 +23,16 @@ config.plugins.autocomplete = common.merge({ max_suggestions = 100, -- Maximum amount of symbols to cache per document max_symbols = 4000, + -- Which symbols to show on the suggestions list: global, local, related, none + suggestions_scope = "global", -- Font size of the description box desc_font_size = 12, + -- Do not show the icons associated to the suggestions + hide_icons = false, + -- Position where icons will be displayed on the suggestions list + icon_position = "left", + -- Do not show the additional information related to a suggestion + hide_info = false, -- The config specification used by gui generators config_spec = { name = "Autocomplete", @@ -60,6 +72,26 @@ config.plugins.autocomplete = common.merge({ min = 1000, max = 10000 }, + { + label = "Suggestions Scope", + description = "Which symbols to show on the suggestions list.", + path = "suggestions_scope", + type = "selection", + default = "global", + values = { + {"All Documents", "global"}, + {"Current Document", "local"}, + {"Related Documents", "related"}, + {"Known Symbols", "none"} + }, + on_apply = function(value) + if value == "global" then + for _, doc in ipairs(core.docs) do + if cache[doc] then cache[doc] = nil end + end + end + end + }, { label = "Description Font Size", description = "Font size of the description box.", @@ -67,6 +99,31 @@ config.plugins.autocomplete = common.merge({ type = "number", default = 12, min = 8 + }, + { + label = "Hide Icons", + description = "Do not show icons on the suggestions list.", + path = "hide_icons", + type = "toggle", + default = false + }, + { + label = "Icons Position", + description = "Position to display icons on the suggestions list.", + path = "icon_position", + type = "selection", + default = "left", + values = { + {"Left", "left"}, + {"Right", "Right"} + } + }, + { + label = "Hide Items Info", + description = "Do not show the additional info related to each suggestion.", + path = "hide_info", + type = "toggle", + default = false } } }, config.plugins.autocomplete) @@ -76,6 +133,7 @@ local autocomplete = {} autocomplete.map = {} autocomplete.map_manually = {} autocomplete.on_close = nil +autocomplete.icons = {} -- Flag that indicates if the autocomplete box was manually triggered -- with the autocomplete.complete() function to prevent the suggestions @@ -95,6 +153,7 @@ function autocomplete.add(t, manually_triggered) { text = text, info = info.info, + icon = info.icon, -- Name of icon to show desc = info.desc, -- Description shown on item selected onhover = info.onhover, -- A callback called once when item is hovered onselect = info.onselect, -- A callback called when item is selected @@ -119,28 +178,35 @@ end -- -- Thread that scans open document symbols and cache them -- -local max_symbols = config.plugins.autocomplete.max_symbols +local global_symbols = {} core.add_thread(function() - local cache = setmetatable({}, { __mode = "k" }) - - local function get_syntax_symbols(symbols, doc) - if doc.syntax then - for sym in pairs(doc.syntax.symbols) do - symbols[sym] = true + local function load_syntax_symbols(doc) + if doc.syntax and not autocomplete.map["language_"..doc.syntax.name] then + local symbols = { + name = "language_"..doc.syntax.name, + files = doc.syntax.files, + items = {} + } + for name, type in pairs(doc.syntax.symbols) do + symbols.items[name] = type end + autocomplete.add(symbols) + return symbols.items end + return {} end local function get_symbols(doc) local s = {} - get_syntax_symbols(s, doc) + local syntax_symbols = load_syntax_symbols(doc) + local max_symbols = config.plugins.autocomplete.max_symbols if doc.disable_symbols then return s end local i = 1 local symbols_count = 0 while i <= #doc.lines do for sym in doc.lines[i]:gmatch(config.symbol_pattern) do - if not s[sym] then + if not s[sym] and not syntax_symbols[sym] then symbols_count = symbols_count + 1 if symbols_count > max_symbols then s = nil @@ -186,14 +252,18 @@ core.add_thread(function() } end -- update symbol set with doc's symbol set - for sym in pairs(cache[doc].symbols) do - symbols[sym] = true + if config.plugins.autocomplete.suggestions_scope == "global" then + for sym in pairs(cache[doc].symbols) do + symbols[sym] = true + end end coroutine.yield() end - -- update symbols list - autocomplete.add { name = "open-docs", items = symbols } + -- update global symbols list + if config.plugins.autocomplete.suggestions_scope == "global" then + global_symbols = symbols + end -- wait for next scan local valid = true @@ -240,12 +310,50 @@ local function update_suggestions() map = autocomplete.map_manually end + local assigned_sym = {} + -- get all relevant suggestions for given filename local items = {} for _, v in pairs(map) do if common.match_pattern(filename, v.files) then for _, item in pairs(v.items) do table.insert(items, item) + assigned_sym[item.text] = true + end + end + end + + -- Append the global, local or related text symbols if applicable + local scope = config.plugins.autocomplete.suggestions_scope + + if not triggered_manually then + local text_symbols = nil + + if scope == "global" then + text_symbols = global_symbols + elseif scope == "local" and cache[doc] and cache[doc].symbols then + text_symbols = cache[doc].symbols + elseif scope == "related" then + for _, d in ipairs(core.docs) do + if doc.syntax == d.syntax then + if cache[d].symbols then + for name in pairs(cache[d].symbols) do + if not assigned_sym[name] then + table.insert(items, setmetatable( + {text = name, info = "normal"}, mt + )) + end + end + end + end + end + end + + if text_symbols then + for name in pairs(text_symbols) do + if not assigned_sym[name] then + table.insert(items, setmetatable({text = name, info = "normal"}, mt)) + end end end end @@ -286,13 +394,23 @@ local function get_suggestions_rect(av) y = y + av:get_line_height() + style.padding.y local font = av:get_font() local th = font:get_height() + local has_icons = false + local hide_info = config.plugins.autocomplete.hide_info + local hide_icons = config.plugins.autocomplete.hide_icons local max_width = 0 for _, s in ipairs(suggestions) do local w = font:get_width(s.text) - if s.info then + if s.info and not hide_info then w = w + style.font:get_width(s.info) + style.padding.x end + local icon = s.icon or s.info + if not hide_icons and icon and autocomplete.icons[icon] then + w = w + autocomplete.icons[icon].font:get_width( + autocomplete.icons[icon].char + ) + (style.padding.x / 2) + has_icons = true + end max_width = math.max(max_width, w) end @@ -319,7 +437,8 @@ local function get_suggestions_rect(av) x - style.padding.x, y - style.padding.y, max_width + style.padding.x * 2, - max_items * (th + style.padding.y) + style.padding.y + max_items * (th + style.padding.y) + style.padding.y, + has_icons end local function wrap_line(line, max_chars) @@ -439,7 +558,7 @@ local function draw_suggestions_box(av) local ah = config.plugins.autocomplete.max_height -- draw background rect - local rx, ry, rw, rh = get_suggestions_rect(av) + local rx, ry, rw, rh, has_icons = get_suggestions_rect(av) renderer.draw_rect(rx, ry, rw, rh, style.background3) -- draw text @@ -448,17 +567,52 @@ local function draw_suggestions_box(av) local y = ry + style.padding.y / 2 local show_count = #suggestions <= ah and #suggestions or ah local start_index = suggestions_idx > ah and (suggestions_idx-(ah-1)) or 1 + local hide_info = config.plugins.autocomplete.hide_info for i=start_index, start_index+show_count-1, 1 do if not suggestions[i] then break end local s = suggestions[i] + + local icon_l_padding, icon_r_padding = 0, 0 + + if has_icons then + local icon = s.icon or s.info + if icon and autocomplete.icons[icon] then + local ifont = autocomplete.icons[icon].font + local itext = autocomplete.icons[icon].char + local icolor = autocomplete.icons[icon].color + if i == suggestions_idx then + icolor = style.accent + elseif type(icolor) == "string" then + icolor = style.syntax[icolor] + end + if config.plugins.autocomplete.icon_position == "left" then + common.draw_text( + ifont, icolor, itext, "left", rx + style.padding.x, y, rw, lh + ) + icon_l_padding = ifont:get_width(itext) + (style.padding.x / 2) + else + common.draw_text( + ifont, icolor, itext, "right", rx, y, rw - style.padding.x, lh + ) + icon_r_padding = ifont:get_width(itext) + (style.padding.x / 2) + end + end + end + local color = (i == suggestions_idx) and style.accent or style.text - common.draw_text(font, color, s.text, "left", rx + style.padding.x, y, rw, lh) - if s.info then + common.draw_text( + font, color, s.text, "left", + rx + icon_l_padding + style.padding.x, y, rw, lh + ) + if s.info and not hide_info then color = (i == suggestions_idx) and style.text or style.dim - common.draw_text(style.font, color, s.info, "right", rx, y, rw - style.padding.x, lh) + common.draw_text( + style.font, color, s.info, "right", + rx, y, rw - icon_r_padding - style.padding.x, lh + ) end y = y + lh if suggestions_idx == i then @@ -619,6 +773,31 @@ function autocomplete.can_complete() return false end +---Register a font icon that can be assigned to completion items. +---@param name string +---@param character string +---@param font? renderer.font +---@param color? string | renderer.color A style.syntax[] name or specific color +function autocomplete.add_icon(name, character, font, color) + local color_type = type(color) + assert( + not color or color_type == "table" + or (color_type == "string" and style.syntax[color]), + "invalid icon color given" + ) + autocomplete.icons[name] = { + char = character, + font = font or style.code_font, + color = color or "keyword" + } +end + +-- +-- Register built-in syntax symbol types icon +-- +for name, _ in pairs(style.syntax) do + autocomplete.add_icon(name, "M", style.icon_font, name) +end -- -- Commands @@ -632,7 +811,6 @@ command.add(predicate, { ["autocomplete:complete"] = function(dv) local doc = dv.doc local item = suggestions[suggestions_idx] - local text = item.text local inserted = false if item.onselect then inserted = item.onselect(suggestions_idx, item) diff --git a/data/plugins/detectindent.lua b/data/plugins/detectindent.lua index 8fad044b..9f22c66b 100644 --- a/data/plugins/detectindent.lua +++ b/data/plugins/detectindent.lua @@ -266,7 +266,7 @@ local function detect_indent_stat(doc) local max_lines = auto_detect_max_lines for i, text in get_non_empty_lines(doc.syntax, doc.lines) do local spaces = text:match("^ +") - if spaces then table.insert(stat, spaces:len()) end + if spaces and #spaces > 1 then table.insert(stat, #spaces) end local tabs = text:match("^\t+") if tabs then tab_count = tab_count + 1 end -- if nothing found for first lines try at least 4 more times diff --git a/data/plugins/drawwhitespace.lua b/data/plugins/drawwhitespace.lua index 753b3bb4..8b8567b3 100644 --- a/data/plugins/drawwhitespace.lua +++ b/data/plugins/drawwhitespace.lua @@ -347,7 +347,7 @@ end command.add(nil, { ["draw-whitespace:toggle"] = function() - config.plugins.drawwhitespace.enabled = not config.drawwhitespace.enabled + config.plugins.drawwhitespace.enabled = not config.plugins.drawwhitespace.enabled end, ["draw-whitespace:disable"] = function() diff --git a/data/plugins/language_cpp.lua b/data/plugins/language_cpp.lua index a26ce868..70489713 100644 --- a/data/plugins/language_cpp.lua +++ b/data/plugins/language_cpp.lua @@ -14,9 +14,9 @@ syntax.add { { pattern = { "/%*", "%*/" }, type = "comment" }, { pattern = { '"', '"', '\\' }, type = "string" }, { pattern = { "'", "'", '\\' }, type = "string" }, - { pattern = "0x%x+", type = "number" }, + { pattern = "0x%x+[%x']*", type = "number" }, { pattern = "%d+[%d%.'eE]*f?", type = "number" }, - { pattern = "%.?%d+f?", type = "number" }, + { pattern = "%.?%d+[%d']*f?", type = "number" }, { pattern = "[%+%-=/%*%^%%<>!~|:&]", type = "operator" }, { pattern = "##", type = "operator" }, { pattern = "struct%s()[%a_][%w_]*", type = {"keyword", "keyword2"} }, diff --git a/data/plugins/language_js.lua b/data/plugins/language_js.lua index f79fece6..307aeecf 100644 --- a/data/plugins/language_js.lua +++ b/data/plugins/language_js.lua @@ -1,24 +1,74 @@ -- mod-version:3 local syntax = require "core.syntax" +-- Regex pattern explanation: +-- This will match / and will look ahead for something that looks like a regex. +-- +-- (?!/) Don't match empty regexes. +-- +-- (?>...) this is using an atomic group to minimize backtracking, as that'd +-- cause "Catastrophic Backtracking" in some cases. +-- +-- [^\\[\/]++ will match anything that's isn't an escape, a start of character +-- class or an end of pattern, without backtracking (the second +). +-- +-- \\. will match anything that's escaped. +-- +-- \[(?:[^\\\]++]|\\.)*+\] will match character classes. +-- +-- /[gmiyuvsd]*\s*[\n,;\)\]\}\.]) will match the end of pattern delimiter, optionally +-- followed by pattern options, and anything that can +-- be after a pattern. +-- +-- Demo with some unit tests (click on the Unit Tests entry): https://regex101.com/r/R0w8Qw/1 +-- Note that it has a couple of changes to make it work on that platform. +local regex_pattern = { + [=[/(?=(?!/)(?:(?>[^\\[\/]++|\\.|\[(?:[^\\\]]++|\\.)*+\])*+)++/[gmiyuvsd]*\s*[\n,;\)\]\}\.])()]=], + "/()[gmiyuvsd]*", "\\" +} + +-- For the moment let's not actually differentiate the insides of the regex, +-- as this will need new token types... +local inner_regex_syntax = { + patterns = { + { pattern = "%(()%?[:!=><]", type = { "string", "string" } }, + { pattern = "[.?+*%(%)|]", type = "string" }, + { pattern = "{%d*,?%d*}", type = "string" }, + { regex = { [=[\[()\^?]=], [=[(?:\]|(?=\n))()]=], "\\" }, + type = { "string", "string" }, + syntax = { -- Inside character class + patterns = { + { pattern = "\\\\", type = "string" }, + { pattern = "\\%]", type = "string" }, + { pattern = "[^%]\n]", type = "string" } + }, + symbols = {} + } + }, + { regex = "\\/", type = "string" }, + { regex = "[^/\n]", type = "string" }, + }, + symbols = {} +} + syntax.add { name = "JavaScript", files = { "%.js$", "%.json$", "%.cson$", "%.mjs$", "%.cjs$" }, comment = "//", block_comment = { "/*", "*/" }, patterns = { - { pattern = "//.*", type = "comment" }, - { pattern = { "/%*", "%*/" }, type = "comment" }, - { pattern = { '/[^= ]', '/', '\\' },type = "string" }, - { pattern = { '"', '"', '\\' }, type = "string" }, - { pattern = { "'", "'", '\\' }, type = "string" }, - { pattern = { "`", "`", '\\' }, type = "string" }, - { pattern = "0x[%da-fA-F_]+n?", type = "number" }, - { pattern = "-?%d+[%d%.eE_n]*", type = "number" }, - { pattern = "-?%.?%d+", type = "number" }, - { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" }, - { pattern = "[%a_][%w_]*%f[(]", type = "function" }, - { pattern = "[%a_][%w_]*", type = "symbol" }, + { pattern = "//.*", type = "comment" }, + { pattern = { "/%*", "%*/" }, type = "comment" }, + { regex = regex_pattern, syntax = inner_regex_syntax, type = {"string", "string"} }, + { pattern = { '"', '"', '\\' }, type = "string" }, + { pattern = { "'", "'", '\\' }, type = "string" }, + { pattern = { "`", "`", '\\' }, type = "string" }, + { pattern = "0x[%da-fA-F_]+n?()%s*()/?", type = {"number", "normal", "operator"} }, + { pattern = "-?%d+[%d%.eE_n]*()%s*()/?", type = {"number", "normal", "operator"} }, + { pattern = "-?%.?%d+()%s*()/?", type = {"number", "normal", "operator"} }, + { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" }, + { pattern = "[%a_][%w_]*%f[(]", type = "function" }, + { pattern = "[%a_][%w_]*()%s*()/?", type = {"symbol", "normal", "operator"} }, }, symbols = { ["async"] = "keyword", diff --git a/data/plugins/language_md.lua b/data/plugins/language_md.lua index 57e4dd52..9ed0a43a 100644 --- a/data/plugins/language_md.lua +++ b/data/plugins/language_md.lua @@ -3,25 +3,6 @@ local syntax = require "core.syntax" local style = require "core.style" local core = require "core" -local initial_color = style.syntax["keyword2"] - --- Add 3 type of font styles for use on markdown files -for _, attr in pairs({"bold", "italic", "bold_italic"}) do - local attributes = {} - if attr ~= "bold_italic" then - attributes[attr] = true - else - attributes["bold"] = true - attributes["italic"] = true - end - style.syntax_fonts["markdown_"..attr] = style.code_font:copy( - style.code_font:get_size(), - attributes - ) - -- also add a color for it - style.syntax["markdown_"..attr] = style.syntax["keyword2"] -end - local in_squares_match = "^%[%]" local in_parenthesis_match = "^%(%)" @@ -225,12 +206,63 @@ syntax.add { -- Adjust the color on theme changes core.add_thread(function() + local custom_fonts = { bold = {font = nil, color = nil}, italic = {}, bold_italic = {} } + local initial_color + local last_code_font + + local function set_font(attr) + local attributes = {} + if attr ~= "bold_italic" then + attributes[attr] = true + else + attributes["bold"] = true + attributes["italic"] = true + end + local font = style.code_font:copy( + style.code_font:get_size(), + attributes + ) + custom_fonts[attr].font = font + style.syntax_fonts["markdown_"..attr] = font + end + + local function set_color(attr) + custom_fonts[attr].color = style.syntax["keyword2"] + style.syntax["markdown_"..attr] = style.syntax["keyword2"] + end + + -- Add 3 type of font styles for use on markdown files + for attr, _ in pairs(custom_fonts) do + -- Only set it if the font wasn't manually customized + if not style.syntax_fonts["markdown_"..attr] then + set_font(attr) + end + + -- Only set it if the color wasn't manually customized + if not style.syntax["markdown_"..attr] then + set_color(attr) + end + end + while true do - if initial_color ~= style.syntax["keyword2"] then - for _, attr in pairs({"bold", "italic", "bold_italic"}) do - style.syntax["markdown_"..attr] = style.syntax["keyword2"] + if last_code_font ~= style.code_font then + last_code_font = style.code_font + for attr, _ in pairs(custom_fonts) do + -- Only set it if the font wasn't manually customized + if style.syntax_fonts["markdown_"..attr] == custom_fonts[attr].font then + set_font(attr) + end end + end + + if initial_color ~= style.syntax["keyword2"] then initial_color = style.syntax["keyword2"] + for attr, _ in pairs(custom_fonts) do + -- Only set it if the color wasn't manually customized + if style.syntax["markdown_"..attr] == custom_fonts[attr].color then + set_color(attr) + end + end end coroutine.yield(1) end diff --git a/data/plugins/linewrapping.lua b/data/plugins/linewrapping.lua index 62412720..0b9f0b76 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.v_scrollbar:get_thumb_rect() + local w = docview.v_scrollbar.expanded_size or style.expanded_scrollbar_size local width = (type(config.plugins.linewrapping.width_override) == "function" and config.plugins.linewrapping.width_override(docview)) or config.plugins.linewrapping.width_override or (docview.size.x - docview:get_gutter_width() - w) if (not docview.wrapped_settings or docview.wrapped_settings.width == nil or width ~= docview.wrapped_settings.width) then diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 2a773edb..9dc65d31 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -29,7 +29,7 @@ local tooltip_alpha_rate = 1 local function get_depth(filename) local n = 1 - for sep in filename:gmatch("[\\/]") do + for _ in filename:gmatch(PATHSEP) do n = n + 1 end return n diff --git a/data/plugins/workspace.lua b/data/plugins/workspace.lua index 6426cbdb..3b3bd044 100644 --- a/data/plugins/workspace.lua +++ b/data/plugins/workspace.lua @@ -83,7 +83,8 @@ local function save_view(view) filename = view.doc.filename, selection = { view.doc:get_selection() }, scroll = { x = view.scroll.to.x, y = view.scroll.to.y }, - text = not view.doc.filename and view.doc:get_text(1, 1, math.huge, math.huge) + crlf = view.doc.crlf, + text = view.doc.new_file and view.doc:get_text(1, 1, math.huge, math.huge) } end if mt == LogView then return end @@ -106,7 +107,6 @@ local function load_view(t) if not t.filename then -- document not associated to a file dv = DocView(core.open_doc()) - if t.text then dv.doc:insert(1, 1, t.text) end else -- we have a filename, try to read the file local ok, doc = pcall(core.open_doc, t.filename) @@ -114,9 +114,11 @@ local function load_view(t) dv = DocView(doc) end end - -- doc view "dv" can be nil here if the filename associated to the document - -- cannot be read. if dv and dv.doc then + if dv.doc.new_file and t.text then + dv.doc:insert(1, 1, t.text) + dv.doc.crlf = t.crlf + end dv.doc:set_selection(table.unpack(t.selection)) dv.last_line1, dv.last_col1, dv.last_line2, dv.last_col2 = dv.doc:get_selection() dv.scroll.x, dv.scroll.to.x = t.scroll.x, t.scroll.x diff --git a/docs/api/dirmonitor.lua b/docs/api/dirmonitor.lua index 439e0ec4..a12b61f1 100644 --- a/docs/api/dirmonitor.lua +++ b/docs/api/dirmonitor.lua @@ -45,10 +45,14 @@ function dirmonitor:unwatch(fd_or_path) end ---edited, removed or added. A file descriptor will be passed to the ---callback in "multiple" mode or a path in "single" mode. --- +---If an error occurred during the callback execution, the error callback will be called with the error object. +---This callback should not manipulate coroutines to avoid deadlocks. +--- ---@param callback dirmonitor.callback +---@param error_callback fun(error: any): nil --- ---@return boolean? changes True when changes were detected. -function dirmonitor:check(callback) end +function dirmonitor:check(callback, error_callback) end --- ---Get the working mode for the current file system monitoring backend. diff --git a/docs/api/system.lua b/docs/api/system.lua index 9e5ef289..ae0f7424 100644 --- a/docs/api/system.lua +++ b/docs/api/system.lua @@ -61,10 +61,10 @@ function system.poll_event() end --- ---Wait until an event is triggered. --- ----@param timeout number Amount of seconds, also supports fractions ----of a second, eg: 0.01 +---@param timeout? number Amount of seconds, also supports fractions +---of a second, eg: 0.01. If not provided, waits forever. --- ----@return boolean status True on success or false if there was an error. +---@return boolean status True on success or false if there was an error or if no event was received. function system.wait_event(timeout) end --- diff --git a/meson.build b/meson.build index c3b0569c..f0614cc8 100644 --- a/meson.build +++ b/meson.build @@ -4,8 +4,7 @@ project('lite-xl', license : 'MIT', meson_version : '>= 0.56', default_options : [ - 'c_std=gnu11', - 'wrap_mode=nofallback' + 'c_std=gnu11' ] ) @@ -84,23 +83,27 @@ if not get_option('source-only') 'lua', # Fedora ] - foreach lua : lua_names - last_lua = (lua == lua_names[-1] or get_option('wrap_mode') == 'forcefallback') - lua_dep = dependency(lua, fallback: last_lua ? ['lua', 'lua_dep'] : [], required : false, - version: '>= 5.4', - default_options: default_fallback_options + ['default_library=static', 'line_editing=false', 'interpreter=false'] - ) - if lua_dep.found() - break - endif + if get_option('use_system_lua') + foreach lua : lua_names + last_lua = (lua == lua_names[-1] or get_option('wrap_mode') == 'forcefallback') + lua_dep = dependency(lua, required : 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 + 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 + else + lua_dep = dependency('', fallback: ['lua', 'lua_dep'], required : true, + default_options: default_fallback_options + ['default_library=static', 'line_editing=disabled', 'interpreter=false'] + ) + endif pcre2_dep = dependency('libpcre2-8', fallback: ['pcre2', 'libpcre2_8'], default_options: default_fallback_options + ['default_library=static', 'grep=false', 'test=false'] @@ -120,6 +123,7 @@ if not get_option('source-only') sdl_options += 'use_atomic=enabled' sdl_options += 'use_threads=enabled' sdl_options += 'use_timers=enabled' + sdl_options += 'with_main=true' # investigate if this is truly needed # Do not remove before https://github.com/libsdl-org/SDL/issues/5413 is released sdl_options += 'use_events=enabled' @@ -152,12 +156,24 @@ if not get_option('source-only') sdl_options += 'use_video_vulkan=disabled' sdl_options += 'use_video_offscreen=disabled' sdl_options += 'use_power=disabled' + sdl_options += 'system_iconv=disabled' sdl_dep = dependency('sdl2', fallback: ['sdl2', 'sdl2_dep'], default_options: default_fallback_options + sdl_options ) - lite_deps = [lua_dep, sdl_dep, freetype_dep, pcre2_dep, libm, libdl] + if host_machine.system() == 'windows' + if sdl_dep.type_name() == 'internal' + sdlmain_dep = dependency('sdl2main', fallback: ['sdl2main_dep']) + else + sdlmain_dep = cc.find_library('SDL2main') + endif + else + sdlmain_dep = dependency('', required: false) + assert(not sdlmain_dep.found(), 'checking if fake dependency has been found') + endif + + lite_deps = [lua_dep, sdl_dep, sdlmain_dep, freetype_dep, pcre2_dep, libm, libdl] endif #=============================================================================== # Install Configuration diff --git a/meson_options.txt b/meson_options.txt index 9cfcc353..ae9d8c6b 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -3,4 +3,5 @@ option('source-only', type : 'boolean', value : false, description: 'Configure s 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', '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 +option('arch_tuple', type : 'string', value : '', description: 'Specify a custom architecture tuple') +option('use_system_lua', type : 'boolean', value : false, description: 'Prefer System Lua over a the meson wrap') diff --git a/resources/README.md b/resources/README.md index 4cd85256..d6d80eec 100644 --- a/resources/README.md +++ b/resources/README.md @@ -11,8 +11,9 @@ This folder contains resources that is used for building or packaging the projec - `icons/icon.{icns,ico,inl,rc,svg}`: lite-xl icon in various formats. - `linux/com.lite_xl.LiteXL.appdata.xml`: AppStream metadata. - `linux/com.lite_xl.LiteXL.desktop`: Desktop file for Linux desktops. -- `macos/appdmg.png`: Background image for packaging MacOS DMGs. -- `macos/Info.plist.in`: Template for generating `info.plist` on MacOS. See `macos/macos-retina-display.md` for details. +- `macos/dmg-cover.png`: Background image for packaging macOS DMGs. +- `macos/Info.plist.in`: Template for generating `info.plist` on macOS. See `macos/macos-retina-display.md` for details. +- `macos/lite-xl-dmg.py`: Configuration options for dmgbuild for packaging macOS DMGs. - `windows/001-lua-unicode.diff`: Patch for allowing Lua to load files with UTF-8 filenames on Windows. ### Development diff --git a/resources/include/lite_xl_plugin_api.h b/resources/include/lite_xl_plugin_api.h index 0c5e93e9..bae9efdd 100644 --- a/resources/include/lite_xl_plugin_api.h +++ b/resources/include/lite_xl_plugin_api.h @@ -2,13 +2,13 @@ * lite_xl_plugin_api.h * API for writing C extension modules loaded by Lite XL. * This file is licensed under MIT. - * + * * The Lite XL plugin API is quite simple. * You would write a lua C extension and replace any references to lua.h, lauxlib.h * and lualib.h with lite_xl_plugin_api.h. * In your main file (where your entrypoint resides), define LITE_XL_PLUGIN_ENTRYPOINT. * If you have multiple entrypoints, define LITE_XL_PLUGIN_ENTRYPOINT in one of them. - * + * * After that, you need to create a Lite XL entrypoint, which is formatted as * luaopen_lite_xl_xxxxx instead of luaopen_xxxxx. * This entrypoint accepts a lua_State and an extra parameter of type void *. @@ -16,9 +16,9 @@ * If you have multiple entrypoints, you must call lite_xl_plugin_init() in * each of them. * This function is not thread safe, so don't try to do anything stupid. - * + * * An example: - * + * * #define LITE_XL_PLUGIN_ENTRYPOINT * #include "lite_xl_plugin_api.h" * int luaopen_lite_xl_xxxxx(lua_State* L, void* XL) { @@ -26,14 +26,19 @@ * ... * return 1; * } - * + * * You can compile the library just like any Lua library without linking to Lua. * An example command would be: gcc -shared -o xxxxx.so xxxxx.c * You must not link to ANY lua library to avoid symbol collision. - * - * This file contains stock configuration for a typical installation of Lua 5.4. + * + * This file contains stock configuration for a typical installation of Lua 5.4.6. * DO NOT MODIFY ANYTHING. MODIFYING STUFFS IN HERE WILL BREAK * COMPATIBILITY WITH LITE XL AND CAUSE UNDEBUGGABLE BUGS. + * + * For reference, here are a list of permalinks to previous version of this file that targets an older version of Lua. + * If you don't need functionalities offered by the new version, use the OLDEST FILE for backwards compatibility. + * + * - Lua 5.4.4: https://github.com/lite-xl/lite-xl/blob/397973067f14420b26e3b20a238a50016c0b75e2/resources/include/lite_xl_plugin_api.h **/ #ifndef LITE_XL_PLUGIN_API #define LITE_XL_PLUGIN_API @@ -60,8 +65,8 @@ #define FE_7(what, x, ...) what x,FE_6(what, __VA_ARGS__) #define FE_8(what, x, ...) what x,FE_7(what, __VA_ARGS__) #define FOR_EACH_NARG(...) FOR_EACH_NARG_(__VA_ARGS__, FOR_EACH_RSEQ_N()) -#define FOR_EACH_NARG_(...) FOR_EACH_ARG_N(__VA_ARGS__) -#define FOR_EACH_ARG_N(_1, _2, _3, _4, _5, _6, _7, _8, N, ...) N +#define FOR_EACH_NARG_(...) FOR_EACH_ARG_N(__VA_ARGS__) +#define FOR_EACH_ARG_N(_1, _2, _3, _4, _5, _6, _7, _8, N, ...) N #define FOR_EACH_RSEQ_N() 8, 7, 6, 5, 4, 3, 2, 1, 0 #define FOR_EACH_(N, what, ...) CONCAT(FE_, N)(what, __VA_ARGS__) #define FOR_EACH(what, ...) FOR_EACH_(FOR_EACH_NARG(__VA_ARGS__), what, __VA_ARGS__) @@ -1028,6 +1033,7 @@ extern const char lua_ident[]; SYMBOL_DECLARE(lua_State *, lua_newstate, lua_Alloc f, void *ud) SYMBOL_DECLARE(void, lua_close, lua_State *L) SYMBOL_DECLARE(lua_State *, lua_newthread, lua_State *L) +SYMBOL_DECLARE(int, lua_closethread, lua_State *L, lua_State *from) SYMBOL_DECLARE(int, lua_resetthread, lua_State *L) SYMBOL_DECLARE(lua_CFunction, lua_atpanic, lua_State *L, lua_CFunction panicf) @@ -1739,6 +1745,9 @@ SYMBOL_WRAP_DECL(void, lua_close, lua_State *L) { SYMBOL_WRAP_DECL(lua_State *, lua_newthread, lua_State *L) { return SYMBOL_WRAP_CALL(lua_newthread, L); } +SYMBOL_WRAP_DECL(int, lua_closethread, lua_State *L, lua_State *from) { + return SYMBOL_WRAP_CALL(lua_closethread, L, from); +} SYMBOL_WRAP_DECL(int, lua_resetthread, lua_State *L) { return SYMBOL_WRAP_CALL(lua_resetthread, L); } @@ -2351,6 +2360,7 @@ void lite_xl_plugin_init(void *XL) { IMPORT_SYMBOL(lua_newstate, lua_State *, lua_Alloc f, void *ud); IMPORT_SYMBOL(lua_close, void, lua_State *L); IMPORT_SYMBOL(lua_newthread, lua_State *, lua_State *L); + IMPORT_SYMBOL(lua_closethread, int, lua_State *L, lua_State *from); IMPORT_SYMBOL(lua_resetthread, int, lua_State *L); IMPORT_SYMBOL(lua_atpanic, lua_CFunction, lua_State *L, lua_CFunction panicf); IMPORT_SYMBOL(lua_version, lua_Number, lua_State *L); @@ -2560,4 +2570,4 @@ void lite_xl_plugin_init(void *XL); * 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. -******************************************************************************/ +******************************************************************************/ diff --git a/resources/macos/appdmg.png b/resources/macos/dmg-cover.png similarity index 100% rename from resources/macos/appdmg.png rename to resources/macos/dmg-cover.png diff --git a/resources/macos/lite-xl-dmg.py b/resources/macos/lite-xl-dmg.py new file mode 100644 index 00000000..27b24417 --- /dev/null +++ b/resources/macos/lite-xl-dmg.py @@ -0,0 +1,28 @@ +# configuration for dmgbuild + +import os.path + +app_path = "Lite XL.app" +app_name = os.path.basename(app_path) + +# Image options +format = defines.get("format", "UDZO") + +# Content options +files = [(app_path, app_name)] +symlinks = { "Applications": "/Applications" } +icon = "resources/icons/icon.icns" +icon_locations = { + app_name: (144, 248), + "Applications": (336, 248) +} + +# Window options +background = "resources/macos/dmg-cover.png" +window_rect = ((360, 360), (480, 380)) +default_view = "coverflow" +include_icon_view_settings = True + +# Icon view options +icon_size = 80 +text_size = 11.0 diff --git a/resources/windows/001-lua-unicode.diff b/resources/windows/001-lua-unicode.diff index 31fec364..9252666f 100644 --- a/resources/windows/001-lua-unicode.diff +++ b/resources/windows/001-lua-unicode.diff @@ -1,6 +1,6 @@ -diff -ruN lua-5.4.4\meson.build lua-5.4.4-patched\meson.build ---- lua-5.4.4\meson.build Wed Feb 22 18:16:56 2023 -+++ lua-5.4.4-patched\meson.build Wed Feb 22 04:10:01 2023 +diff -ruN lua-5.4.4/meson.build lua-5.4.4-patched/meson.build +--- lua-5.4.4/meson.build Wed Feb 22 18:16:56 2023 ++++ lua-5.4.4-patched/meson.build Wed Feb 22 04:10:01 2023 @@ -85,6 +85,7 @@ 'src/lutf8lib.c', 'src/lvm.c', @@ -9,13 +9,13 @@ diff -ruN lua-5.4.4\meson.build lua-5.4.4-patched\meson.build dependencies: lua_lib_deps, version: meson.project_version(), soversion: lua_versions[0] + '.' + lua_versions[1], -diff -ruN lua-5.4.4\src\luaconf.h lua-5.4.4-patched\src\luaconf.h ---- lua-5.4.4\src\luaconf.h Thu Jan 13 19:24:43 2022 -+++ lua-5.4.4-patched\src\luaconf.h Wed Feb 22 04:10:02 2023 +diff -ruN lua-5.4.4/src/luaconf.h lua-5.4.4-patched/src/luaconf.h +--- lua-5.4.4/src/luaconf.h Thu Jan 13 19:24:43 2022 ++++ lua-5.4.4-patched/src/luaconf.h Wed Feb 22 04:10:02 2023 @@ -782,5 +782,15 @@ - - - + + + +#if defined(lua_c) || defined(luac_c) || (defined(LUA_LIB) && \ + (defined(lauxlib_c) || defined(liolib_c) || \ + defined(loadlib_c) || defined(loslib_c))) @@ -27,22 +27,22 @@ diff -ruN lua-5.4.4\src\luaconf.h lua-5.4.4-patched\src\luaconf.h + + #endif - -diff -ruN lua-5.4.4\src\Makefile lua-5.4.4-patched\src\Makefile ---- lua-5.4.4\src\Makefile Thu Jul 15 22:01:52 2021 -+++ lua-5.4.4-patched\src\Makefile Wed Feb 22 04:10:02 2023 + +diff -ruN lua-5.4.4/src/Makefile lua-5.4.4-patched/src/Makefile +--- lua-5.4.4/src/Makefile Thu Jul 15 22:01:52 2021 ++++ lua-5.4.4-patched/src/Makefile Wed Feb 22 04:10:02 2023 @@ -33,7 +33,7 @@ PLATS= guess aix bsd c89 freebsd generic linux linux-readline macosx mingw posix solaris - + 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.4\src\utf8_wrappers.c lua-5.4.4-patched\src\utf8_wrappers.c ---- lua-5.4.4\src\utf8_wrappers.c Thu Jan 01 08:00:00 1970 -+++ lua-5.4.4-patched\src\utf8_wrappers.c Wed Feb 22 18:13:45 2023 + +diff -ruN lua-5.4.4/src/utf8_wrappers.c lua-5.4.4-patched/src/utf8_wrappers.c +--- lua-5.4.4/src/utf8_wrappers.c Thu Jan 01 08:00:00 1970 ++++ lua-5.4.4-patched/src/utf8_wrappers.c Wed Feb 22 18:13:45 2023 @@ -0,0 +1,129 @@ +/** + * Wrappers to provide Unicode (UTF-8) support on Windows. @@ -173,9 +173,9 @@ diff -ruN lua-5.4.4\src\utf8_wrappers.c lua-5.4.4-patched\src\utf8_wrappers.c + return env_value; +} +#endif -diff -ruN lua-5.4.4\src\utf8_wrappers.h lua-5.4.4-patched\src\utf8_wrappers.h ---- lua-5.4.4\src\utf8_wrappers.h Thu Jan 01 08:00:00 1970 -+++ lua-5.4.4-patched\src\utf8_wrappers.h Wed Feb 22 18:09:48 2023 +diff -ruN lua-5.4.4/src/utf8_wrappers.h lua-5.4.4-patched/src/utf8_wrappers.h +--- lua-5.4.4/src/utf8_wrappers.h Thu Jan 01 08:00:00 1970 ++++ lua-5.4.4-patched/src/utf8_wrappers.h Wed Feb 22 18:09:48 2023 @@ -0,0 +1,46 @@ +/** + * Wrappers to provide Unicode (UTF-8) support on Windows. diff --git a/scripts/README.md b/scripts/README.md index 478e461f..5910931c 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -10,7 +10,7 @@ Various scripts and configurations used to configure, build, and package Lite XL ### Package -- **appdmg.sh**: Create a macOS DMG image using [AppDMG][1]. +- **appdmg.sh**: Create a macOS DMG image using [dmgbuild][1]. - **appimage.sh**: [AppImage][2] builder. - **innosetup.sh**: Creates a 32/64 bit [InnoSetup][3] installer package. - **package.sh**: Creates all binary / DMG image / installer / source packages. @@ -25,6 +25,6 @@ Various scripts and configurations used to configure, build, and package Lite XL - **generate_header.sh**: Generates a header file for native plugin API - **keymap-generator**: Generates a JSON file containing the keymap -[1]: https://github.com/LinusU/node-appdmg +[1]: https://github.com/dmgbuild/dmgbuild [2]: https://docs.appimage.org/ [3]: https://jrsoftware.org/isinfo.php diff --git a/scripts/appdmg.sh b/scripts/appdmg.sh index 840f518b..5ce4fe71 100644 --- a/scripts/appdmg.sh +++ b/scripts/appdmg.sh @@ -6,25 +6,4 @@ if [ ! -e "src/api/api.h" ]; then exit 1 fi -cat > lite-xl-dmg.json << EOF -{ - "title": "Lite XL", - "icon": "$(pwd)/resources/icons/icon.icns", - "background": "$(pwd)/resources/macos/appdmg.png", - "window": { - "position": { - "x": 360, - "y": 360 - }, - "size": { - "width": 480, - "height": 360 - } - }, - "contents": [ - { "x": 144, "y": 248, "type": "file", "path": "$(pwd)/Lite XL.app" }, - { "x": 336, "y": 248, "type": "link", "path": "/Applications" } - ] -} -EOF -~/node_modules/appdmg/bin/appdmg.js lite-xl-dmg.json "$(pwd)/$1.dmg" +dmgbuild -s resources/macos/lite-xl-dmg.py "Lite XL" "$1.dmg" diff --git a/scripts/build.sh b/scripts/build.sh index 778f596d..0908ab19 100644 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -181,7 +181,7 @@ main() { # download the subprojects so we can start patching before configure. # this will prevent reconfiguring the project. meson subprojects download - lua_subproject_path=$(echo subprojects/lua-*/) + lua_subproject_path="subprojects/$(awk -F ' *= *' '/directory/ { printf $2 }' subprojects/lua.wrap)" if [[ -d $lua_subproject_path ]]; then patch -d $lua_subproject_path -p1 --forward < resources/windows/001-lua-unicode.diff fi diff --git a/scripts/innosetup/innosetup.iss.in b/scripts/innosetup/innosetup.iss.in index 628733ff..ff1cf425 100644 --- a/scripts/innosetup/innosetup.iss.in +++ b/scripts/innosetup/innosetup.iss.in @@ -1,7 +1,7 @@ #define MyAppName "Lite XL" #define MyAppVersion "@PROJECT_VERSION@" #define MyAppPublisher "Lite XL Team" -#define MyAppURL "https://lite-xl.github.io" +#define MyAppURL "https://lite-xl.com" #define MyAppExeName "lite-xl.exe" #define BuildDir "@PROJECT_BUILD_DIR@" #define SourceDir "@PROJECT_SOURCE_DIR@" @@ -57,9 +57,13 @@ OutputBaseFilename=LiteXL-{#MyAppVersion}-{#ArchInternal}-setup LicenseFile={#SourceDir}/LICENSE SetupIconFile={#SourceDir}/resources/icons/icon.ico +UninstallDisplayIcon={app}\{#MyAppExeName}, 0 WizardImageFile="{#SourceDir}/scripts/innosetup/wizard-modern-image.bmp" WizardSmallImageFile="{#SourceDir}/scripts/innosetup/litexl-55px.bmp" +; Required for the add to path option to refresh environment +ChangesEnvironment=yes + [Languages] Name: "english"; MessagesFile: "compiler:Default.isl" @@ -67,11 +71,10 @@ Name: "english"; MessagesFile: "compiler:Default.isl" Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 6.1; Check: not IsAdminInstallMode Name: "portablemode"; Description: "Portable Mode"; Flags: unchecked +Name: "envPath"; Description: "Add lite-xl to the PATH variable, allowing it to be run from a command line." [Files] -Source: "{#BuildDir}/src/lite-xl.exe"; DestDir: "{app}"; Flags: ignoreversion -Source: "{#BuildDir}/mingwLibs{#Arch}/*"; DestDir: "{app}"; Flags: ignoreversion ; Check: DirExists(ExpandConstant('{#BuildDir}/mingwLibs{#Arch}')) -Source: "{#SourceDir}/data/*"; DestDir: "{app}/data"; Flags: ignoreversion recursesubdirs +Source: "{#SourceDir}/lite-xl/*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs ; NOTE: Don't use "Flags: ignoreversion" on any shared system files [Icons] @@ -81,8 +84,78 @@ Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"; Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon; Check: not WizardIsTaskSelected('portablemode') ; Name: "{usersendto}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" +[Registry] +Root: "HKA"; Subkey: "Software\Classes\*\shell\{#MyAppName}"; ValueType: string; ValueName: ""; ValueData: "Open with {#MyAppName}"; Flags: uninsdeletekey +Root: "HKA"; Subkey: "Software\Classes\*\shell\{#MyAppName}"; ValueType: string; ValueName: "Icon"; ValueData: "{app}\{#MyAppExeName}, 0"; Flags: uninsdeletekey +Root: "HKA"; Subkey: "Software\Classes\*\shell\{#MyAppName}\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExename}"" ""%1"""; Flags: uninsdeletekey +Root: "HKA"; Subkey: "Software\Classes\directory\shell\{#MyAppName}"; ValueType: string; ValueName: ""; ValueData: "Open with {#MyAppName}"; Flags: uninsdeletekey +Root: "HKA"; Subkey: "Software\Classes\directory\shell\{#MyAppName}"; ValueType: string; ValueName: "Icon"; ValueData: "{app}\{#MyAppExeName}, 0"; Flags: uninsdeletekey +Root: "HKA"; Subkey: "Software\Classes\directory\shell\{#MyAppName}\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExename}"" ""%1"""; Flags: uninsdeletekey +Root: "HKA"; Subkey: "Software\Classes\directory\background\shell\{#MyAppName}"; ValueType: string; ValueName: ""; ValueData: "Open with {#MyAppName}"; Flags: uninsdeletekey +Root: "HKA"; Subkey: "Software\Classes\directory\background\shell\{#MyAppName}"; ValueType: string; ValueName: "Icon"; ValueData: "{app}\{#MyAppExeName}, 0"; Flags: uninsdeletekey +Root: "HKA"; Subkey: "Software\Classes\directory\background\shell\{#MyAppName}\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExename}"" ""%V"""; Flags: uninsdeletekey + [Run] Filename: "{app}/{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent [Setup] Uninstallable=not WizardIsTaskSelected('portablemode') + +; Code to add installation path to environment taken from: +; https://stackoverflow.com/a/46609047 +[Code] +const EnvironmentKey = 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment'; + +procedure EnvAddPath(Path: string); +var + Paths: string; +begin + { Retrieve current path (use empty string if entry not exists) } + if not RegQueryStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths) + then Paths := ''; + + { Skip if string already found in path } + if Pos(';' + Uppercase(Path) + ';', ';' + Uppercase(Paths) + ';') > 0 then exit; + + { App string to the end of the path variable } + Paths := Paths + ';'+ Path +';' + + { Overwrite (or create if missing) path environment variable } + if RegWriteStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths) + then Log(Format('The [%s] added to PATH: [%s]', [Path, Paths])) + else Log(Format('Error while adding the [%s] to PATH: [%s]', [Path, Paths])); +end; + +procedure EnvRemovePath(Path: string); +var + Paths: string; + P: Integer; +begin + { Skip if registry entry not exists } + if not RegQueryStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths) then + exit; + + { Skip if string not found in path } + P := Pos(';' + Uppercase(Path) + ';', ';' + Uppercase(Paths) + ';'); + if P = 0 then exit; + + { Update path variable } + Delete(Paths, P - 1, Length(Path) + 1); + + { Overwrite path environment variable } + if RegWriteStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths) + then Log(Format('The [%s] removed from PATH: [%s]', [Path, Paths])) + else Log(Format('Error while removing the [%s] from PATH: [%s]', [Path, Paths])); +end; + +procedure CurStepChanged(CurStep: TSetupStep); +begin + if (CurStep = ssPostInstall) and WizardIsTaskSelected('envPath') + then EnvAddPath(ExpandConstant('{app}')); +end; + +procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); +begin + if CurUninstallStep = usPostUninstall + then EnvRemovePath(ExpandConstant('{app}')); +end; diff --git a/scripts/install-dependencies.sh b/scripts/install-dependencies.sh index 1290cfcc..00606944 100644 --- a/scripts/install-dependencies.sh +++ b/scripts/install-dependencies.sh @@ -57,9 +57,7 @@ main() { else brew install bash ninja sdl2 fi - pip3 install meson - cd ~; npm install appdmg; cd - - ~/node_modules/appdmg/bin/appdmg.js --version + pip3 install meson dmgbuild elif [[ "$OSTYPE" == "msys" ]]; then if [[ $lhelper == true ]]; then pacman --noconfirm -S \ diff --git a/scripts/package.sh b/scripts/package.sh index d2346b76..d9a9c14c 100644 --- a/scripts/package.sh +++ b/scripts/package.sh @@ -25,7 +25,7 @@ show_help() { echo "-A --appimage Create an AppImage (Linux only)." echo "-B --binary Create a normal / portable package or macOS bundle," echo " depending on how the build was configured. (Default.)" - echo "-D --dmg Create a DMG disk image with AppDMG (macOS only)." + echo "-D --dmg Create a DMG disk image with dmgbuild (macOS only)." echo "-I --innosetup Create a InnoSetup package (Windows only)." echo "-r --release Strip debugging symbols." echo "-S --source Create a source code package," @@ -264,6 +264,11 @@ main() { $stripcmd "${exe_file}" fi + if [[ $bundle == true ]]; then + # https://eclecticlight.co/2019/01/17/code-signing-for-the-concerned-3-signing-an-app/ + codesign --force --deep -s - "${dest_dir}" + fi + echo "Creating a compressed archive ${package_name}" if [[ $binary == true ]]; then rm -f "${package_name}".tar.gz diff --git a/src/api/dirmonitor.c b/src/api/dirmonitor.c index 1bccfd13..320235a0 100644 --- a/src/api/dirmonitor.c +++ b/src/api/dirmonitor.c @@ -1,4 +1,5 @@ #include "api.h" +#include "lua.h" #include #include #include @@ -25,13 +26,16 @@ int get_mode_dirmonitor(); static int f_check_dir_callback(int watch_id, const char* path, void* L) { - lua_pushvalue(L, -1); + // using absolute indices from f_dirmonitor_check (2: callback, 3: error_callback) + lua_pushvalue(L, 2); if (path) lua_pushlstring(L, path, watch_id); else lua_pushnumber(L, watch_id); - lua_call(L, 1, 1); - int result = lua_toboolean(L, -1); + + int result = 0; + if (lua_pcall(L, 1, 1, 3) == LUA_OK) + result = lua_toboolean(L, -1); lua_pop(L, 1); return !result; } @@ -95,8 +99,20 @@ static int f_dirmonitor_unwatch(lua_State *L) { } +static int f_noop(lua_State *L) { return 0; } + + static int f_dirmonitor_check(lua_State* L) { struct dirmonitor* monitor = luaL_checkudata(L, 1, API_TYPE_DIRMONITOR); + luaL_checktype(L, 2, LUA_TFUNCTION); + if (!lua_isnoneornil(L, 3)) { + luaL_checktype(L, 3, LUA_TFUNCTION); + } else { + lua_settop(L, 2); + lua_pushcfunction(L, f_noop); + } + lua_settop(L, 3); + SDL_LockMutex(monitor->mutex); if (monitor->length < 0) lua_pushnil(L); diff --git a/src/api/process.c b/src/api/process.c index 796b2c5f..2ab24317 100644 --- a/src/api/process.c +++ b/src/api/process.c @@ -34,6 +34,8 @@ typedef DWORD process_error_t; typedef HANDLE process_stream_t; typedef HANDLE process_handle_t; +typedef wchar_t process_arglist_t[32767]; +typedef wchar_t *process_env_t; #define HANDLE_INVALID (INVALID_HANDLE_VALUE) #define PROCESS_GET_HANDLE(P) ((P)->process_information.hProcess) @@ -45,12 +47,20 @@ static volatile long PipeSerialNumber; typedef int process_error_t; typedef int process_stream_t; typedef pid_t process_handle_t; +typedef char **process_arglist_t; +typedef char **process_env_t; #define HANDLE_INVALID (0) #define PROCESS_GET_HANDLE(P) ((P)->pid) #endif +#ifdef __GNUC__ +#define UNUSED __attribute__((__unused__)) +#else +#define UNUSED +#endif + typedef struct { bool running, detached; int returncode, deadline; @@ -342,51 +352,264 @@ static bool signal_process(process_t* proc, signal_e sig) { return true; } -static int process_start(lua_State* L) { - int retval = 1; - size_t env_len = 0, key_len, val_len; - const char *cmd[256] = { NULL }, *env_names[256] = { NULL }, *env_values[256] = { NULL }, *cwd = NULL; - bool detach = false, literal = false; - int deadline = 10, new_fds[3] = { STDIN_FD, STDOUT_FD, STDERR_FD }; - size_t arg_len = lua_gettop(L), cmd_len; - if (lua_type(L, 1) == LUA_TTABLE) { - #if LUA_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); - if (!cmd_len) - // we have not allocated anything here yet, so we can skip cleanup code - // don't do this anywhere else! - return luaL_argerror(L, 1,"table cannot be empty"); - for (size_t i = 1; i <= cmd_len; ++i) { - lua_pushinteger(L, i); - lua_rawget(L, 1); - cmd[i-1] = luaL_checkstring(L, -1); - } + +static UNUSED char *xstrdup(const char *str) { + char *result = str ? malloc(strlen(str) + 1) : NULL; + if (result) strcpy(result, str); + return result; +} + + +static int process_arglist_init(process_arglist_t *list, size_t *list_len, size_t nargs) { + *list_len = 0; +#ifdef _WIN32 + memset(*list, 0, sizeof(process_arglist_t)); +#else + *list = calloc(sizeof(char *), nargs + 1); + if (!*list) return ENOMEM; +#endif + return 0; +} + + +static int process_arglist_add(process_arglist_t *list, size_t *list_len, const char *arg, bool escape) { + size_t len = *list_len; +#ifdef _WIN32 + int arg_len; + wchar_t *cmdline = *list; + wchar_t arg_w[32767]; + // this length includes the null terminator! + if (!(arg_len = MultiByteToWideChar(CP_UTF8, 0, arg, -1, arg_w, 32767))) + return GetLastError(); + if (arg_len + len > 32767) + return ERROR_NOT_ENOUGH_MEMORY; + + if (!escape) { + // replace the current null terminator with a space + if (len > 0) cmdline[len-1] = ' '; + memcpy(cmdline + len, arg_w, arg_len * sizeof(wchar_t)); + len += arg_len; } else { - literal = true; - cmd[0] = luaL_checkstring(L, 1); - cmd_len = 1; + // if the string contains spaces, then we must quote it + bool quote = wcspbrk(arg_w, L" \t\v\r\n"); + int backslash = 0, escaped_len = quote ? 2 : 0; + for (int i = 0; i < arg_len; i++) { + if (arg_w[i] == L'\\') { + backslash++; + } else if (arg_w[i] == L'"') { + escaped_len += backslash + 1; + backslash = 0; + } else { + backslash = 0; + } + escaped_len++; + } + // escape_len contains NUL terminator + if (escaped_len + len > 32767) + return ERROR_NOT_ENOUGH_MEMORY; + // replace our previous NUL terminator with space + if (len > 0) cmdline[len-1] = L' '; + if (quote) cmdline[len++] = L'"'; + // we are not going to iterate over NUL terminator + for (int i = 0;arg_w[i]; i++) { + if (arg_w[i] == L'\\') { + backslash++; + } else if (arg_w[i] == L'"') { + // add backslash + 1 backslashes + for (int j = 0; j < backslash; j++) + cmdline[len++] = L'\\'; + cmdline[len++] = L'\\'; + backslash = 0; + } else { + backslash = 0; + } + cmdline[len++] = arg_w[i]; + } + if (quote) cmdline[len++] = L'"'; + cmdline[len++] = L'\0'; + } +#else + char **cmd = *list; + cmd[len] = xstrdup(arg); + if (!cmd[len]) return ENOMEM; + len++; +#endif + *list_len = len; + return 0; +} + + +static void process_arglist_free(process_arglist_t *list) { +#ifndef _WIN32 + char **cmd = *list; + for (int i = 0; cmd[i]; i++) + free(cmd[i]); + free(cmd); + *list = NULL; +#endif +} + + +static int process_env_init(process_env_t *env_list, size_t *env_len, size_t nenv) { + *env_len = 0; +#ifdef _WIN32 + *env_list = NULL; +#else + *env_list = calloc(sizeof(char *), nenv * 2); + if (!*env_list) return ENOMEM; +#endif + return 0; +} + + +#ifdef _WIN32 +static int cmp_name(wchar_t *a, wchar_t *b) { + wchar_t _A[32767], _B[32767], *A = _A, *B = _B, *a_eq, *b_eq; + int na, nb, r; + a_eq = wcschr(a, L'='); + b_eq = wcschr(b, L'='); + assert(a_eq); + assert(b_eq); + na = a_eq - a; + nb = b_eq - b; + r = LCMapStringW(LOCALE_INVARIANT, LCMAP_UPPERCASE, a, na, A, na); + assert(r == na); + A[na] = L'\0'; + r = LCMapStringW(LOCALE_INVARIANT, LCMAP_UPPERCASE, b, nb, B, nb); + assert(r == nb); + B[nb] = L'\0'; + + for (;;) { + wchar_t AA = *A++, BB = *B++; + if (AA > BB) + return 1; + else if (AA < BB) + return -1; + else if (!AA && !BB) + return 0; + } +} + + +static int process_env_add_variable(process_env_t *env_list, size_t *env_list_len, wchar_t *var, size_t var_len) { + wchar_t *list, *list_p; + size_t block_var_len, list_len; + list = list_p = *env_list; + list_len = *env_list_len; + if (list_len) { + // check if it is already in the block + while ((block_var_len = wcslen(list_p))) { + if (cmp_name(list_p, var) == 0) + return -1; // already installed + list_p += block_var_len + 1; + } + } + // allocate list + 1 characters for the block terminator + list = realloc(list, (list_len + var_len + 1) * sizeof(wchar_t)); + if (!list) return ERROR_NOT_ENOUGH_MEMORY; + // copy the env variable to the block + memcpy(list + list_len, var, var_len * sizeof(wchar_t)); + // terminate the block again + list[list_len + var_len] = L'\0'; + *env_list = list; + *env_list_len = (list_len + var_len); + return 0; +} + + +static int process_env_add_system(process_env_t *env_list, size_t *env_list_len) { + int retval = 0; + wchar_t *proc_env_block, *proc_env_block_p; + int proc_env_len; + + proc_env_block = proc_env_block_p = GetEnvironmentStringsW(); + while ((proc_env_len = wcslen(proc_env_block_p))) { + // try to add it to the list + if ((retval = process_env_add_variable(env_list, env_list_len, proc_env_block_p, proc_env_len + 1)) > 0) + goto cleanup; + proc_env_block_p += proc_env_len + 1; + } + retval = 0; + +cleanup: + if (proc_env_block) FreeEnvironmentStringsW(proc_env_block); + return retval; +} +#endif + + +static int process_env_add(process_env_t *env_list, size_t *env_len, const char *key, const char *value) { +#ifdef _WIN32 + wchar_t env_var[32767]; + int r, var_len = 0; + if (!(r = MultiByteToWideChar(CP_UTF8, 0, key, -1, env_var, 32767))) + return GetLastError(); + var_len += r; + env_var[var_len-1] = L'='; + if (!(r = MultiByteToWideChar(CP_UTF8, 0, value, -1, env_var + var_len, 32767 - var_len))) + return GetLastError(); + var_len += r; + return process_env_add_variable(env_list, env_len, env_var, var_len); +#else + (*env_list)[*env_len] = xstrdup(key); + if (!(*env_list)[*env_len]) + return ENOMEM; + (*env_list)[*env_len + 1] = xstrdup(value); + if (!(*env_list)[*env_len + 1]) + return ENOMEM; + *env_len += 2; +#endif + return 0; +} + + +static void process_env_free(process_env_t *list) { + if (!*list) return; +#ifdef _WIN32 + free(*list); +#else + for (size_t i = 0; (*list)[i]; i++) free((*list)[i]); + free(*list); +#endif + *list = NULL; +} + + +static int process_start(lua_State* L) { + int r, retval = 1; + size_t env_len = 0, cmd_len = 0, arglist_len = 0, env_vars_len = 0; + process_arglist_t arglist; + process_env_t env_vars = NULL; + const char *cwd = NULL; + bool detach = false, escape = true; + int deadline = 10, new_fds[3] = { STDIN_FD, STDOUT_FD, STDERR_FD }; + + if (lua_isstring(L, 1)) { + escape = false; + // create a table that contains the string as the value + lua_createtable(L, 1, 0); + lua_pushvalue(L, 1); + lua_rawseti(L, -2, 1); + lua_replace(L, 1); } - if (arg_len > 1) { - lua_getfield(L, 2, "env"); - if (!lua_isnil(L, -1)) { - lua_pushnil(L); - while (lua_next(L, -2) != 0) { - const char* key = luaL_checklstring(L, -2, &key_len); - const char* val = luaL_checklstring(L, -1, &val_len); - env_names[env_len] = malloc(key_len+1); - strcpy((char*)env_names[env_len], key); - env_values[env_len] = malloc(val_len+1); - strcpy((char*)env_values[env_len], val); - lua_pop(L, 1); - ++env_len; - } - } else - lua_pop(L, 1); + 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 + cmd_len = luaL_checknumber(L, -1); lua_pop(L, 1); + if (!cmd_len) + return luaL_argerror(L, 1, "table cannot be empty"); + // check if each arguments is a string + for (size_t i = 1; i <= cmd_len; ++i) { + lua_rawgeti(L, 1, i); + luaL_checkstring(L, -1); + lua_pop(L, 1); + } + + if (lua_istable(L, 2)) { lua_getfield(L, 2, "detach"); detach = lua_toboolean(L, -1); lua_getfield(L, 2, "timeout"); deadline = luaL_optnumber(L, -1, deadline); lua_getfield(L, 2, "cwd"); cwd = luaL_optstring(L, -1, NULL); @@ -394,20 +617,70 @@ static int process_start(lua_State* L) { lua_getfield(L, 2, "stdout"); new_fds[STDOUT_FD] = luaL_optnumber(L, -1, STDOUT_FD); lua_getfield(L, 2, "stderr"); new_fds[STDERR_FD] = luaL_optnumber(L, -1, STDERR_FD); for (int stream = STDIN_FD; stream <= STDERR_FD; ++stream) { - if (new_fds[stream] > STDERR_FD || new_fds[stream] < REDIRECT_PARENT) { - lua_pushfstring(L, "error: redirect to handles, FILE* and paths are not supported"); + if (new_fds[stream] > STDERR_FD || new_fds[stream] < REDIRECT_PARENT) + return luaL_error(L, "error: redirect to handles, FILE* and paths are not supported"); + } + lua_pop(L, 6); // pop all the values above + + luaL_getsubtable(L, 2, "env"); + // count environment variobles + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + luaL_checkstring(L, -2); + luaL_checkstring(L, -1); + lua_pop(L, 1); + env_len++; + } + + if (env_len) { + if ((r = process_env_init(&env_vars, &env_vars_len, env_len)) != 0) { retval = -1; + push_error(L, "cannot allocate environment list", r); goto cleanup; } + + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + if ((r = process_env_add(&env_vars, &env_vars_len, lua_tostring(L, -2), lua_tostring(L, -1))) != 0) { + retval = -1; + push_error(L, "cannot copy environment variable", r); + goto cleanup; + } + lua_pop(L, 1); + env_len++; + } } } + // allocate and copy commands + if ((r = process_arglist_init(&arglist, &arglist_len, cmd_len)) != 0) { + retval = -1; + push_error(L, "cannot create argument list", r); + goto cleanup; + } + for (size_t i = 1; i <= cmd_len; i++) { + lua_rawgeti(L, 1, i); + if ((r = process_arglist_add(&arglist, &arglist_len, lua_tostring(L, -1), escape)) != 0) { + retval = -1; + push_error(L, "cannot add argument", r); + goto cleanup; + } + lua_pop(L, 1); + } + process_t* self = lua_newuserdata(L, sizeof(process_t)); memset(self, 0, sizeof(process_t)); luaL_setmetatable(L, API_TYPE_PROCESS); self->deadline = deadline; self->detached = detach; #if _WIN32 + if (env_vars) { + if ((r = process_env_add_system(&env_vars, &env_vars_len)) != 0) { + retval = -1; + push_error(L, "cannot add environment variable", r); + goto cleanup; + } + } for (int i = 0; i < 3; ++i) { switch (new_fds[i]) { case REDIRECT_PARENT: @@ -458,7 +731,7 @@ static int process_start(lua_State* L) { self->child_pipes[i][1] = self->child_pipes[new_fds[i]][1]; } } - STARTUPINFO siStartInfo; + STARTUPINFOW siStartInfo; memset(&self->process_information, 0, sizeof(self->process_information)); memset(&siStartInfo, 0, sizeof(siStartInfo)); siStartInfo.cb = sizeof(siStartInfo); @@ -466,48 +739,10 @@ static int process_start(lua_State* L) { siStartInfo.hStdInput = self->child_pipes[STDIN_FD][0]; siStartInfo.hStdOutput = self->child_pipes[STDOUT_FD][1]; siStartInfo.hStdError = self->child_pipes[STDERR_FD][1]; - char commandLine[32767] = { 0 }, environmentBlock[32767], wideEnvironmentBlock[32767*2]; - int offset = 0; - if (!literal) { - for (size_t i = 0; i < cmd_len; ++i) { - size_t len = strlen(cmd[i]); - if (offset + len + 2 >= sizeof(commandLine)) break; - if (i > 0) - commandLine[offset++] = ' '; - commandLine[offset++] = '"'; - int backslashCount = 0; // Yes, this is necessary. - for (size_t j = 0; j < len && offset + 2 + backslashCount < sizeof(commandLine); ++j) { - if (cmd[i][j] == '\\') - ++backslashCount; - else if (cmd[i][j] == '"') { - for (size_t k = 0; k < backslashCount; ++k) - commandLine[offset++] = '\\'; - commandLine[offset++] = '\\'; - backslashCount = 0; - } else - backslashCount = 0; - commandLine[offset++] = cmd[i][j]; - } - if (offset + 1 + backslashCount >= sizeof(commandLine)) break; - for (size_t k = 0; k < backslashCount; ++k) - commandLine[offset++] = '\\'; - commandLine[offset++] = '"'; - } - commandLine[offset] = 0; - } else { - strncpy(commandLine, cmd[0], sizeof(commandLine)); - } - offset = 0; - for (size_t i = 0; i < env_len; ++i) { - if (offset + strlen(env_values[i]) + strlen(env_names[i]) + 1 >= sizeof(environmentBlock)) - break; - offset += snprintf(&environmentBlock[offset], sizeof(environmentBlock) - offset, "%s=%s", env_names[i], env_values[i]); - environmentBlock[offset++] = 0; - } - environmentBlock[offset++] = 0; - if (env_len > 0) - MultiByteToWideChar(CP_UTF8, MB_PRECOMPOSED, environmentBlock, offset, (LPWSTR)wideEnvironmentBlock, sizeof(wideEnvironmentBlock)); - if (!CreateProcess(NULL, commandLine, NULL, NULL, true, (detach ? DETACHED_PROCESS : CREATE_NO_WINDOW) | CREATE_UNICODE_ENVIRONMENT, env_len > 0 ? wideEnvironmentBlock : NULL, cwd, &siStartInfo, &self->process_information)) { + wchar_t cwd_w[MAX_PATH]; + if (cwd) // TODO: error handling + MultiByteToWideChar(CP_UTF8, 0, cwd, -1, cwd_w, MAX_PATH); + if (!CreateProcessW(NULL, arglist, NULL, NULL, true, (detach ? DETACHED_PROCESS : CREATE_NO_WINDOW) | CREATE_UNICODE_ENVIRONMENT, env_vars, cwd ? cwd_w : NULL, &siStartInfo, &self->process_information)) { push_error(L, NULL, GetLastError()); retval = -1; goto cleanup; @@ -555,9 +790,9 @@ static int process_start(lua_State* L) { close(self->child_pipes[stream][stream == STDIN_FD ? 1 : 0]); } size_t set; - for (set = 0; set < env_len && setenv(env_names[set], env_values[set], 1) == 0; ++set); - if (set == env_len && (!detach || setsid() != -1) && (!cwd || chdir(cwd) != -1)) - execvp(cmd[0], (char** const)cmd); + for (set = 0; set < env_vars_len && setenv(env_vars[set], env_vars[set+1], 1) == 0; set += 2); + if (set == env_vars_len && (!detach || setsid() != -1) && (!cwd || chdir(cwd) != -1)) + execvp(arglist[0], (char** const)arglist); write(control_pipe[1], &errno, sizeof(errno)); _exit(-1); } @@ -591,16 +826,15 @@ static int process_start(lua_State* L) { if (control_pipe[0]) close(control_pipe[0]); if (control_pipe[1]) close(control_pipe[1]); #endif - for (size_t i = 0; i < env_len; ++i) { - free((char*)env_names[i]); - free((char*)env_values[i]); - } for (int stream = 0; stream < 3; ++stream) { process_stream_t* pipe = &self->child_pipes[stream][stream == STDIN_FD ? 0 : 1]; if (*pipe) { close_fd(pipe); } } + process_arglist_free(&arglist); + process_env_free(&env_vars); + if (retval == -1) return lua_error(L); @@ -741,6 +975,10 @@ static int self_signal(lua_State* L, signal_e sig) { static int f_terminate(lua_State* L) { return self_signal(L, SIGNAL_TERM); } 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); } +<<<<<<< HEAD +======= + +>>>>>>> master static int f_gc(lua_State* L) { process_kill_list_t *list = NULL; process_kill_t *p = NULL; diff --git a/src/api/renderer.c b/src/api/renderer.c index 871e18a4..2d8aaca4 100644 --- a/src/api/renderer.c +++ b/src/api/renderer.c @@ -90,7 +90,7 @@ static int f_font_load(lua_State *L) { return ret_code; RenFont** font = lua_newuserdata(L, sizeof(RenFont*)); - *font = ren_font_load(&window_renderer, filename, size, antialiasing, hinting, style); + *font = ren_font_load(window_renderer, filename, size, antialiasing, hinting, style); if (!*font) return luaL_error(L, "failed to load font"); luaL_setmetatable(L, API_TYPE_FONT); @@ -130,7 +130,7 @@ static int f_font_copy(lua_State *L) { } for (int i = 0; i < FONT_FALLBACK_MAX && fonts[i]; ++i) { RenFont** font = lua_newuserdata(L, sizeof(RenFont*)); - *font = ren_font_copy(&window_renderer, fonts[i], size, antialiasing, hinting, style); + *font = ren_font_copy(window_renderer, fonts[i], size, antialiasing, hinting, style); if (!*font) return luaL_error(L, "failed to copy font"); luaL_setmetatable(L, API_TYPE_FONT); @@ -198,7 +198,7 @@ static int f_font_get_width(lua_State *L) { size_t len; const char *text = luaL_checklstring(L, 2, &len); - lua_pushnumber(L, ren_font_group_get_width(&window_renderer, fonts, text, len)); + lua_pushnumber(L, ren_font_group_get_width(window_renderer, fonts, text, len, NULL)); return 1; } @@ -217,7 +217,7 @@ static int f_font_get_size(lua_State *L) { static int f_font_set_size(lua_State *L) { RenFont* fonts[FONT_FALLBACK_MAX]; font_retrieve(L, fonts, 1); float size = luaL_checknumber(L, 2); - ren_font_group_set_size(&window_renderer, fonts, size); + ren_font_group_set_size(window_renderer, fonts, size); return 0; } @@ -276,7 +276,7 @@ static int f_show_debug(lua_State *L) { static int f_get_size(lua_State *L) { int w, h; - ren_get_size(&window_renderer, &w, &h); + ren_get_size(window_renderer, &w, &h); lua_pushnumber(L, w); lua_pushnumber(L, h); return 2; @@ -284,13 +284,13 @@ static int f_get_size(lua_State *L) { static int f_begin_frame(UNUSED lua_State *L) { - rencache_begin_frame(&window_renderer); + rencache_begin_frame(window_renderer); return 0; } static int f_end_frame(UNUSED lua_State *L) { - rencache_end_frame(&window_renderer); + rencache_end_frame(window_renderer); // clear the font reference table lua_newtable(L); lua_rawseti(L, LUA_REGISTRYINDEX, RENDERER_FONT_REF); @@ -311,7 +311,7 @@ static int f_set_clip_rect(lua_State *L) { lua_Number w = luaL_checknumber(L, 3); lua_Number h = luaL_checknumber(L, 4); RenRect rect = rect_to_grid(x, y, w, h); - rencache_set_clip_rect(&window_renderer, rect); + rencache_set_clip_rect(window_renderer, rect); return 0; } @@ -323,7 +323,7 @@ static int f_draw_rect(lua_State *L) { lua_Number h = luaL_checknumber(L, 4); RenRect rect = rect_to_grid(x, y, w, h); RenColor color = checkcolor(L, 5, 255); - rencache_draw_rect(&window_renderer, rect, color); + rencache_draw_rect(window_renderer, rect, color); return 0; } @@ -348,7 +348,7 @@ static int f_draw_text(lua_State *L) { double x = luaL_checknumber(L, 3); int y = luaL_checknumber(L, 4); RenColor color = checkcolor(L, 5, 255); - x = rencache_draw_text(&window_renderer, fonts, text, len, x, y, color); + x = rencache_draw_text(window_renderer, fonts, text, len, x, y, color); lua_pushnumber(L, x); return 1; } diff --git a/src/api/system.c b/src/api/system.c index c5fdf382..384f2580 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -78,7 +78,7 @@ static SDL_HitTestResult SDLCALL hit_test(SDL_Window *window, const SDL_Point *p const int controls_width = hit_info->controls_width; int w, h; - SDL_GetWindowSize(window_renderer.window, &w, &h); + SDL_GetWindowSize(window_renderer->window, &w, &h); if (pt->y < hit_info->title_height && #if RESIZE_FROM_TOP @@ -197,7 +197,7 @@ top: case SDL_WINDOWEVENT: if (e.window.event == SDL_WINDOWEVENT_RESIZED) { - ren_resize_window(&window_renderer); + ren_resize_window(window_renderer); lua_pushstring(L, "resized"); /* The size below will be in points. */ lua_pushinteger(L, e.window.data1); @@ -236,8 +236,8 @@ top: SDL_GetMouseState(&mx, &my); lua_pushstring(L, "filedropped"); lua_pushstring(L, e.drop.file); - lua_pushinteger(L, mx); - lua_pushinteger(L, my); + lua_pushinteger(L, mx * window_renderer->scale_x); + lua_pushinteger(L, my * window_renderer->scale_y); SDL_free(e.drop.file); return 4; @@ -294,8 +294,8 @@ top: if (e.button.button == 1) { SDL_CaptureMouse(1); } lua_pushstring(L, "mousepressed"); lua_pushstring(L, button_name(e.button.button)); - lua_pushinteger(L, e.button.x); - lua_pushinteger(L, e.button.y); + lua_pushinteger(L, e.button.x * window_renderer->scale_x); + lua_pushinteger(L, e.button.y * window_renderer->scale_y); lua_pushinteger(L, e.button.clicks); return 5; @@ -303,8 +303,8 @@ top: if (e.button.button == 1) { SDL_CaptureMouse(0); } lua_pushstring(L, "mousereleased"); lua_pushstring(L, button_name(e.button.button)); - lua_pushinteger(L, e.button.x); - lua_pushinteger(L, e.button.y); + lua_pushinteger(L, e.button.x * window_renderer->scale_x); + lua_pushinteger(L, e.button.y * window_renderer->scale_y); return 4; case SDL_MOUSEMOTION: @@ -316,10 +316,10 @@ top: e.motion.yrel += event_plus.motion.yrel; } lua_pushstring(L, "mousemoved"); - lua_pushinteger(L, e.motion.x); - lua_pushinteger(L, e.motion.y); - lua_pushinteger(L, e.motion.xrel); - lua_pushinteger(L, e.motion.yrel); + lua_pushinteger(L, e.motion.x * window_renderer->scale_x); + lua_pushinteger(L, e.motion.y * window_renderer->scale_y); + lua_pushinteger(L, e.motion.xrel * window_renderer->scale_x); + lua_pushinteger(L, e.motion.yrel * window_renderer->scale_y); return 5; case SDL_MOUSEWHEEL: @@ -335,7 +335,7 @@ top: return 3; case SDL_FINGERDOWN: - SDL_GetWindowSize(window_renderer.window, &w, &h); + SDL_GetWindowSize(window_renderer->window, &w, &h); lua_pushstring(L, "touchpressed"); lua_pushinteger(L, (lua_Integer)(e.tfinger.x * w)); @@ -344,7 +344,7 @@ top: return 4; case SDL_FINGERUP: - SDL_GetWindowSize(window_renderer.window, &w, &h); + SDL_GetWindowSize(window_renderer->window, &w, &h); lua_pushstring(L, "touchreleased"); lua_pushinteger(L, (lua_Integer)(e.tfinger.x * w)); @@ -360,7 +360,7 @@ top: e.tfinger.dx += event_plus.tfinger.dx; e.tfinger.dy += event_plus.tfinger.dy; } - SDL_GetWindowSize(window_renderer.window, &w, &h); + SDL_GetWindowSize(window_renderer->window, &w, &h); lua_pushstring(L, "touchmoved"); lua_pushinteger(L, (lua_Integer)(e.tfinger.x * w)); @@ -374,7 +374,7 @@ top: #ifdef LITE_USE_SDL_RENDERER rencache_invalidate(); #else - SDL_UpdateWindowSurface(window_renderer.window); + SDL_UpdateWindowSurface(window_renderer->window); #endif lua_pushstring(L, e.type == SDL_APP_WILLENTERFOREGROUND ? "enteringforeground" : "enteredforeground"); return 1; @@ -397,6 +397,7 @@ static int f_wait_event(lua_State *L) { int nargs = lua_gettop(L); if (nargs >= 1) { double n = luaL_checknumber(L, 1); + if (n < 0) n = 0; lua_pushboolean(L, SDL_WaitEventTimeout(NULL, n * 1000)); } else { lua_pushboolean(L, SDL_WaitEvent(NULL)); @@ -439,7 +440,7 @@ static int f_set_cursor(lua_State *L) { static int f_set_window_title(lua_State *L) { const char *title = luaL_checkstring(L, 1); - SDL_SetWindowTitle(window_renderer.window, title); + SDL_SetWindowTitle(window_renderer->window, title); return 0; } @@ -449,39 +450,39 @@ enum { WIN_NORMAL, WIN_MINIMIZED, WIN_MAXIMIZED, WIN_FULLSCREEN }; static int f_set_window_mode(lua_State *L) { int n = luaL_checkoption(L, 1, "normal", window_opts); - SDL_SetWindowFullscreen(window_renderer.window, + SDL_SetWindowFullscreen(window_renderer->window, n == WIN_FULLSCREEN ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0); - if (n == WIN_NORMAL) { SDL_RestoreWindow(window_renderer.window); } - if (n == WIN_MAXIMIZED) { SDL_MaximizeWindow(window_renderer.window); } - if (n == WIN_MINIMIZED) { SDL_MinimizeWindow(window_renderer.window); } + if (n == WIN_NORMAL) { SDL_RestoreWindow(window_renderer->window); } + if (n == WIN_MAXIMIZED) { SDL_MaximizeWindow(window_renderer->window); } + if (n == WIN_MINIMIZED) { SDL_MinimizeWindow(window_renderer->window); } return 0; } static int f_set_window_bordered(lua_State *L) { int bordered = lua_toboolean(L, 1); - SDL_SetWindowBordered(window_renderer.window, bordered); + SDL_SetWindowBordered(window_renderer->window, bordered); return 0; } static int f_set_window_hit_test(lua_State *L) { if (lua_gettop(L) == 0) { - SDL_SetWindowHitTest(window_renderer.window, NULL, NULL); + SDL_SetWindowHitTest(window_renderer->window, NULL, NULL); return 0; } window_hit_info->title_height = luaL_checknumber(L, 1); window_hit_info->controls_width = luaL_checknumber(L, 2); window_hit_info->resize_border = luaL_checknumber(L, 3); - SDL_SetWindowHitTest(window_renderer.window, hit_test, window_hit_info); + SDL_SetWindowHitTest(window_renderer->window, hit_test, window_hit_info); return 0; } static int f_get_window_size(lua_State *L) { int x, y, w, h; - SDL_GetWindowSize(window_renderer.window, &w, &h); - SDL_GetWindowPosition(window_renderer.window, &x, &y); + SDL_GetWindowSize(window_renderer->window, &w, &h); + SDL_GetWindowPosition(window_renderer->window, &x, &y); lua_pushinteger(L, w); lua_pushinteger(L, h); lua_pushinteger(L, x); @@ -495,22 +496,22 @@ static int f_set_window_size(lua_State *L) { double h = luaL_checknumber(L, 2); double x = luaL_checknumber(L, 3); double y = luaL_checknumber(L, 4); - SDL_SetWindowSize(window_renderer.window, w, h); - SDL_SetWindowPosition(window_renderer.window, x, y); - ren_resize_window(&window_renderer); + SDL_SetWindowSize(window_renderer->window, w, h); + SDL_SetWindowPosition(window_renderer->window, x, y); + ren_resize_window(window_renderer); return 0; } static int f_window_has_focus(lua_State *L) { - unsigned flags = SDL_GetWindowFlags(window_renderer.window); + unsigned flags = SDL_GetWindowFlags(window_renderer->window); lua_pushboolean(L, flags & SDL_WINDOW_INPUT_FOCUS); return 1; } static int f_get_window_mode(lua_State *L) { - unsigned flags = SDL_GetWindowFlags(window_renderer.window); + unsigned flags = SDL_GetWindowFlags(window_renderer->window); if (flags & SDL_WINDOW_FULLSCREEN_DESKTOP) { lua_pushstring(L, "fullscreen"); } else if (flags & SDL_WINDOW_MINIMIZED) { @@ -548,8 +549,8 @@ static int f_raise_window(lua_State *L) { to allow the window to be focused. Also on wayland the raise window event may not always be obeyed. */ - SDL_SetWindowInputFocus(window_renderer.window); - SDL_RaiseWindow(window_renderer.window); + SDL_SetWindowInputFocus(window_renderer->window); + SDL_RaiseWindow(window_renderer->window); return 0; } @@ -876,6 +877,7 @@ static int f_get_time(lua_State *L) { static int f_sleep(lua_State *L) { double n = luaL_checknumber(L, 1); + if (n < 0) n = 0; SDL_Delay(n * 1000); return 0; } @@ -929,7 +931,7 @@ static int f_fuzzy_match(lua_State *L) { static int f_set_window_opacity(lua_State *L) { double n = luaL_checknumber(L, 1); - int r = SDL_SetWindowOpacity(window_renderer.window, n); + int r = SDL_SetWindowOpacity(window_renderer->window, n); lua_pushboolean(L, r > -1); return 1; } @@ -1071,7 +1073,7 @@ static int f_load_native_plugin(lua_State *L) { #endif /* Special purpose filepath compare function. Corresponds to the - order used in the TreeView view of the project's files. Returns true iff + order used in the TreeView view of the project's files. Returns true if path1 < path2 in the TreeView order. */ static int f_path_compare(lua_State *L) { size_t len1, len2; diff --git a/src/main.c b/src/main.c index 4ffb6603..741d7ae5 100644 --- a/src/main.c +++ b/src/main.c @@ -35,16 +35,6 @@ static SDL_Window *window; -static double get_scale(void) { -#ifndef __APPLE__ - float dpi; - if (SDL_GetDisplayDPI(0, NULL, &dpi, NULL) == 0) - return dpi / 96.0; -#endif - return 1.0; -} - - static void get_exe_filename(char *buf, int sz) { #if _WIN32 int len; @@ -204,6 +194,8 @@ int main(int argc, char **argv) { SDL_SetHint("SDL_MOUSE_DOUBLE_CLICK_RADIUS", "4"); #endif + SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software"); + SDL_DisplayMode dm; SDL_GetCurrentDisplayMode(0, &dm); @@ -217,7 +209,7 @@ int main(int argc, char **argv) { fprintf(stderr, "Error creating lite-xl window: %s", SDL_GetError()); exit(1); } - ren_init(window); + window_renderer = ren_init(window); lua_State *L; init_lua: @@ -239,9 +231,6 @@ init_lua: lua_pushstring(L, LITE_ARCH_TUPLE); lua_setglobal(L, "ARCH"); - lua_pushnumber(L, get_scale()); - lua_setglobal(L, "SCALE"); - char exename[2048]; get_exe_filename(exename, sizeof(exename)); if (*exename) { @@ -314,7 +303,7 @@ init_lua: // This allows the window to be destroyed before lite-xl is done with // reaping child processes - ren_free_window_resources(&window_renderer); + ren_free(window_renderer); lua_close(L); #if defined(__amigaos4__) diff --git a/src/rencache.c b/src/rencache.c index d28ed543..a77f82ca 100644 --- a/src/rencache.c +++ b/src/rencache.c @@ -191,8 +191,9 @@ void rencache_draw_rect(RenWindow *window_renderer, RenRect rect, RenColor color double rencache_draw_text(RenWindow *window_renderer, RenFont **fonts, const char *text, size_t len, double x, int y, RenColor color) { - double width = ren_font_group_get_width(window_renderer, fonts, text, len); - RenRect rect = { x, y, (int)width, ren_font_group_get_height(fonts) }; + int x_offset; + double width = ren_font_group_get_width(window_renderer, fonts, text, len, &x_offset); + RenRect rect = { x + x_offset, y, (int)(width - x_offset), ren_font_group_get_height(fonts) }; if (rects_overlap(last_clip_rect, rect)) { int sz = len + 1; DrawTextCommand *cmd = push_command(window_renderer, DRAW_TEXT, sizeof(DrawTextCommand) + sz); diff --git a/src/renderer.c b/src/renderer.c index a9bea504..5a96eb29 100644 --- a/src/renderer.c +++ b/src/renderer.c @@ -22,7 +22,7 @@ #define MAX_LOADABLE_GLYPHSETS (MAX_UNICODE / GLYPHSET_SIZE) #define SUBPIXEL_BITMAPS_CACHED 3 -RenWindow window_renderer = {0}; +RenWindow* window_renderer = NULL; static FT_Library library; // draw_rect_surface is used as a 1x1 surface to simplify ren_draw_rect with blending @@ -167,7 +167,7 @@ static void font_load_glyphset(RenFont* font, int idx) { for (unsigned int column = 0; column < slot->bitmap.width; ++column) { int current_source_offset = source_offset + (column / 8); int source_pixel = slot->bitmap.buffer[current_source_offset]; - pixels[++target_offset] = ((source_pixel >> (7 - (column % 8))) & 0x1) << 7; + pixels[++target_offset] = ((source_pixel >> (7 - (column % 8))) & 0x1) * 0xFF; } } else memcpy(&pixels[target_offset], &slot->bitmap.buffer[source_offset], slot->bitmap.width); @@ -348,10 +348,11 @@ int ren_font_group_get_height(RenFont **fonts) { return fonts[0]->height; } -double ren_font_group_get_width(RenWindow *window_renderer, RenFont **fonts, const char *text, size_t len) { +double ren_font_group_get_width(RenWindow *window_renderer, RenFont **fonts, const char *text, size_t len, int *x_offset) { double width = 0; const char* end = text + len; GlyphMetric* metric = NULL; GlyphSet* set = NULL; + bool set_x_offset = x_offset == NULL; while (text < end) { unsigned int codepoint; text = utf8_to_codepoint(text, &codepoint); @@ -359,8 +360,15 @@ double ren_font_group_get_width(RenWindow *window_renderer, RenFont **fonts, con if (!metric) break; width += (!font || metric->xadvance) ? metric->xadvance : fonts[0]->space_advance; + if (!set_x_offset) { + set_x_offset = true; + *x_offset = metric->bitmap_left; // TODO: should this be scaled by the surface scale? + } } const int surface_scale = renwin_get_surface(window_renderer).scale; + if (!set_x_offset) { + *x_offset = 0; + } return width / surface_scale; } @@ -493,33 +501,38 @@ void ren_draw_rect(RenSurface *rs, RenRect rect, RenColor color) { } /*************** Window Management ****************/ -void ren_free_window_resources(RenWindow *window_renderer) { +RenWindow* ren_init(SDL_Window *win) { + assert(win); + int error = FT_Init_FreeType( &library ); + if ( error ) { + fprintf(stderr, "internal font error when starting the application\n"); + return NULL; + } + RenWindow* window_renderer = malloc(sizeof(RenWindow)); + + window_renderer->window = win; + renwin_init_surface(window_renderer); + renwin_init_command_buf(window_renderer); + renwin_clip_to_surface(window_renderer); + draw_rect_surface = SDL_CreateRGBSurface(0, 1, 1, 32, + 0xFF000000, 0x00FF0000, 0x0000FF00, 0x000000FF); + + return window_renderer; +} + +void ren_free(RenWindow* window_renderer) { + assert(window_renderer); renwin_free(window_renderer); SDL_FreeSurface(draw_rect_surface); free(window_renderer->command_buf); window_renderer->command_buf = NULL; window_renderer->command_buf_size = 0; + free(window_renderer); } -// TODO remove global and return RenWindow* -void ren_init(SDL_Window *win) { - assert(win); - int error = FT_Init_FreeType( &library ); - if ( error ) { - fprintf(stderr, "internal font error when starting the application\n"); - return; - } - window_renderer.window = win; - renwin_init_surface(&window_renderer); - renwin_init_command_buf(&window_renderer); - renwin_clip_to_surface(&window_renderer); - draw_rect_surface = SDL_CreateRGBSurface(0, 1, 1, 32, - 0xFF000000, 0x00FF0000, 0x0000FF00, 0x000000FF); -} - - void ren_resize_window(RenWindow *window_renderer) { renwin_resize_surface(window_renderer); + renwin_update_scale(window_renderer); } diff --git a/src/renderer.h b/src/renderer.h index 0e96d9a9..78811341 100644 --- a/src/renderer.h +++ b/src/renderer.h @@ -23,7 +23,7 @@ typedef struct { SDL_Surface *surface; int scale; } RenSurface; struct RenWindow; typedef struct RenWindow RenWindow; -extern RenWindow window_renderer; +extern RenWindow* window_renderer; RenFont* ren_font_load(RenWindow *window_renderer, const char *filename, float size, ERenFontAntialiasing antialiasing, ERenFontHinting hinting, unsigned char style); RenFont* ren_font_copy(RenWindow *window_renderer, RenFont* font, float size, ERenFontAntialiasing antialiasing, ERenFontHinting hinting, int style); @@ -34,17 +34,17 @@ int ren_font_group_get_height(RenFont **font); float ren_font_group_get_size(RenFont **font); void ren_font_group_set_size(RenWindow *window_renderer, RenFont **font, float size); void ren_font_group_set_tab_size(RenFont **font, int n); -double ren_font_group_get_width(RenWindow *window_renderer, RenFont **font, const char *text, size_t len); +double ren_font_group_get_width(RenWindow *window_renderer, RenFont **font, const char *text, size_t len, int *x_offset); double ren_draw_text(RenSurface *rs, RenFont **font, const char *text, size_t len, float x, int y, RenColor color); void ren_draw_rect(RenSurface *rs, RenRect rect, RenColor color); -void ren_init(SDL_Window *win); +RenWindow* ren_init(SDL_Window *win); +void ren_free(RenWindow* window_renderer); void ren_resize_window(RenWindow *window_renderer); void ren_update_rects(RenWindow *window_renderer, RenRect *rects, int count); void ren_set_clip_rect(RenWindow *window_renderer, RenRect rect); void ren_get_size(RenWindow *window_renderer, int *x, int *y); /* Reports the size in points. */ -void ren_free_window_resources(RenWindow *window_renderer); #endif diff --git a/src/renwindow.c b/src/renwindow.c index cdfab9a6..03a89e6b 100644 --- a/src/renwindow.c +++ b/src/renwindow.c @@ -41,7 +41,8 @@ static void setup_renderer(RenWindow *ren, int w, int h) { #endif -void renwin_init_surface(UNUSED RenWindow *ren) { +void renwin_init_surface(RenWindow *ren) { + ren->scale_x = ren->scale_y = 1; #ifdef LITE_USE_SDL_RENDERER if (ren->rensurface.surface) { SDL_FreeSurface(ren->rensurface.surface); @@ -107,6 +108,16 @@ void renwin_resize_surface(UNUSED RenWindow *ren) { #endif } +void renwin_update_scale(RenWindow *ren) { +#ifndef LITE_USE_SDL_RENDERER + SDL_Surface *surface = SDL_GetWindowSurface(ren->window); + int window_w = surface->w, window_h = surface->h; + SDL_GetWindowSize(ren->window, &window_w, &window_h); + ren->scale_x = (float)surface->w / window_w; + ren->scale_y = (float)surface->h / window_h; +#endif +} + void renwin_show_window(RenWindow *ren) { SDL_ShowWindow(ren->window); } diff --git a/src/renwindow.h b/src/renwindow.h index a80586a0..364950de 100644 --- a/src/renwindow.h +++ b/src/renwindow.h @@ -6,6 +6,8 @@ struct RenWindow { uint8_t *command_buf; size_t command_buf_idx; size_t command_buf_size; + float scale_x; + float scale_y; #ifdef LITE_USE_SDL_RENDERER SDL_Renderer *renderer; SDL_Texture *texture; @@ -19,6 +21,7 @@ void renwin_init_command_buf(RenWindow *ren); void renwin_clip_to_surface(RenWindow *ren); void renwin_set_clip_rect(RenWindow *ren, RenRect rect); void renwin_resize_surface(RenWindow *ren); +void renwin_update_scale(RenWindow *ren); void renwin_show_window(RenWindow *ren); void renwin_update_rects(RenWindow *ren, RenRect *rects, int count); void renwin_free(RenWindow *ren); diff --git a/subprojects/freetype2.wrap b/subprojects/freetype2.wrap index 1ff89ecd..e3554c9a 100644 --- a/subprojects/freetype2.wrap +++ b/subprojects/freetype2.wrap @@ -1,9 +1,10 @@ [wrap-file] -directory = freetype-2.12.1 -source_url = https://download.savannah.gnu.org/releases/freetype/freetype-2.12.1.tar.xz -source_filename = freetype-2.12.1.tar.xz -source_hash = 4766f20157cc4cf0cd292f80bf917f92d1c439b243ac3018debf6b9140c41a7f -wrapdb_version = 2.12.1-2 +directory = freetype-2.13.2 +source_url = https://download.savannah.gnu.org/releases/freetype/freetype-2.13.2.tar.xz +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/freetype2_2.13.2-1/freetype-2.13.2.tar.xz +source_filename = freetype-2.13.2.tar.xz +source_hash = 12991c4e55c506dd7f9b765933e62fd2be2e06d421505d7950a132e4f1bb484d +wrapdb_version = 2.13.2-1 [provide] freetype2 = freetype_dep diff --git a/subprojects/lua.wrap b/subprojects/lua.wrap index 5a2d615b..16502616 100644 --- a/subprojects/lua.wrap +++ b/subprojects/lua.wrap @@ -1,12 +1,14 @@ [wrap-file] -directory = lua-5.4.4 -source_url = https://www.lua.org/ftp/lua-5.4.4.tar.gz -source_filename = lua-5.4.4.tar.gz -source_hash = 164c7849653b80ae67bec4b7473b884bf5cc8d2dca05653475ec2ed27b9ebf61 -patch_filename = lua_5.4.4-1_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/lua_5.4.4-1/get_patch -patch_hash = e61cd965c629d6543176f41a9f1cb9050edfd1566cf00ce768ff211086e40bdc +directory = lua-5.4.6 +source_url = https://www.lua.org/ftp/lua-5.4.6.tar.gz +source_filename = lua-5.4.6.tar.gz +source_hash = 7d5ea1b9cb6aa0b59ca3dde1c6adcb57ef83a1ba8e5432c0ecd06bf439b3ad88 +patch_filename = lua_5.4.6-3_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/lua_5.4.6-3/get_patch +patch_hash = 9b72a95422fd47f79f969d9abdb589ee95712d5512a5246f94e7e4f63d2cb7b7 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/lua_5.4.6-3/lua-5.4.6.tar.gz +wrapdb_version = 5.4.6-3 [provide] lua-5.4 = lua_dep - +lua = lua_dep diff --git a/subprojects/pcre2.wrap b/subprojects/pcre2.wrap index 8fada34e..6bfef6f3 100644 --- a/subprojects/pcre2.wrap +++ b/subprojects/pcre2.wrap @@ -1,12 +1,13 @@ [wrap-file] directory = pcre2-10.42 -source_url = https://github.com/PhilipHazel/pcre2/releases/download/pcre2-10.42/pcre2-10.42.tar.bz2 +source_url = https://github.com/PCRE2Project/pcre2/releases/download/pcre2-10.42/pcre2-10.42.tar.bz2 source_filename = pcre2-10.42.tar.bz2 source_hash = 8d36cd8cb6ea2a4c2bb358ff6411b0c788633a2a45dabbf1aeb4b701d1b5e840 -patch_filename = pcre2_10.42-1_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/pcre2_10.42-1/get_patch -patch_hash = 06969e916dfee663c189810df57d98574f15e0754a44cd93f3f0bc7234b05d89 -wrapdb_version = 10.42-1 +patch_filename = pcre2_10.42-5_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/pcre2_10.42-5/get_patch +patch_hash = 7ba1730a3786c46f41735658a9884b09bc592af3840716e0ccc552e7ddf5630c +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/pcre2_10.42-5/pcre2-10.42.tar.bz2 +wrapdb_version = 10.42-5 [provide] libpcre2-8 = libpcre2_8 diff --git a/subprojects/sdl2.wrap b/subprojects/sdl2.wrap index 53a71e2f..14c18533 100644 --- a/subprojects/sdl2.wrap +++ b/subprojects/sdl2.wrap @@ -1,12 +1,15 @@ [wrap-file] -directory = SDL2-2.26.0 -source_url = https://github.com/libsdl-org/SDL/releases/download/release-2.26.0/SDL2-2.26.0.tar.gz -source_filename = SDL2-2.26.0.tar.gz -source_hash = 8000d7169febce93c84b6bdf376631f8179132fd69f7015d4dadb8b9c2bdb295 -patch_filename = sdl2_2.26.0-1_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/sdl2_2.26.0-1/get_patch -patch_hash = 6fcfd727d71cf7837332723518d5e47ffd64f1e7630681cf4b50e99f2bf7676f -wrapdb_version = 2.26.0-1 +directory = SDL2-2.28.1 +source_url = https://github.com/libsdl-org/SDL/releases/download/release-2.28.1/SDL2-2.28.1.tar.gz +source_filename = SDL2-2.28.1.tar.gz +source_hash = 4977ceba5c0054dbe6c2f114641aced43ce3bf2b41ea64b6a372d6ba129cb15d +patch_filename = sdl2_2.28.1-2_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/sdl2_2.28.1-2/get_patch +patch_hash = 2dd332226ba2a4373c6d4eb29fa915e9d5414cf7bb9fa2e4a5ef3b16a06e2736 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/sdl2_2.28.1-2/SDL2-2.28.1.tar.gz +wrapdb_version = 2.28.1-2 [provide] sdl2 = sdl2_dep +sdl2main = sdl2main_dep +sdl2_test = sdl2_test_dep