diff --git a/build-packages.sh b/build-packages.sh index b9a65d64..b7cccfa6 100755 --- a/build-packages.sh +++ b/build-packages.sh @@ -62,6 +62,7 @@ lite_build_package_windows () { local bindir="$pdir" local datadir="$pdir/data" else + echo "WARNING: using non portable option on unix-like system" local bindir="$pdir/bin" local datadir="$pdir/share/lite-xl" fi @@ -70,6 +71,9 @@ lite_build_package_windows () { for module_name in core plugins colors fonts; do copy_directory_from_repo --strip-components=1 "data/$module_name" "$datadir" done + for module_name in plugins colors; do + cp -r "$build/third/data/$module_name" "$datadir" + done cp "$build/src/lite.exe" "$bindir" strip --strip-all "$bindir/lite.exe" pushd ".package-build" @@ -100,6 +104,9 @@ lite_build_package_macosx () { for module_name in core plugins colors fonts; do copy_directory_from_repo --strip-components=1 "data/$module_name" "$datadir" done + for module_name in plugins colors; do + cp -r "$build/third/data/$module_name" "$datadir" + done cp "$build/src/lite" "$bindir" strip "$bindir/lite" pushd ".package-build" @@ -119,6 +126,7 @@ lite_build_package_linux () { local os="linux" local pdir=".package-build/lite-xl" if [ $portable == "true" ]; then + echo "WARNING: using portable option on unix-like system" local bindir="$pdir" local datadir="$pdir/data" else @@ -130,6 +138,9 @@ lite_build_package_linux () { for module_name in core plugins colors fonts; do copy_directory_from_repo --strip-components=1 "data/$module_name" "$datadir" done + for module_name in plugins colors; do + cp -r "$build/third/data/$module_name" "$datadir" + done cp "$build/src/lite" "$bindir" strip "$bindir/lite" pushd ".package-build" @@ -154,6 +165,15 @@ lite_build_package () { fi } +lite_copy_third_party_modules () { + local build="$1" + curl --insecure -L "https://github.com/rxi/lite-colors/archive/master.zip" -o "$build/rxi-lite-colors.zip" + mkdir -p "$build/third/data/colors" "$build/third/data/plugins" + unzip "$build/rxi-lite-colors.zip" -d "$build" + mv "$build/lite-colors-master/colors" "$build/third/data" + rm -fr "$build/lite-colors-master" +} + if [[ -z "$1" || -z "$2" ]]; then echo "usage: $0 [options] " exit 1 @@ -184,5 +204,6 @@ if [ -z ${pgo+set} ]; then else lite_build_pgo "$portable" "$build_dir" fi +lite_copy_third_party_modules "$build_dir" lite_build_package "$portable" "$build_dir" "$version" "$arch" diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index a4cf4d7d..cf904ccf 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -12,6 +12,10 @@ command.add(nil, { core.quit() end, + ["core:restart"] = function() + core.restart() + end, + ["core:force-quit"] = function() core.quit(true) end, @@ -98,7 +102,15 @@ command.add(nil, { end, ["core:open-user-module"] = function() - core.root_view:open_doc(core.open_doc(USERDIR .. "/init.lua")) + local user_module_doc = core.open_doc(USERDIR .. "/init.lua") + if not user_module_doc then return end + local doc_save = user_module_doc.save + user_module_doc.save = function(self) + doc_save(self) + core.reload_module("core.style") + core.load_user_directory() + end + core.root_view:open_doc(user_module_doc) end, ["core:open-project-module"] = function() @@ -111,4 +123,32 @@ command.add(nil, { doc:save(filename) end end, + + ["core:change-project-folder"] = function() + core.command_view:enter("Change Project Folder", function(text) + local path_stat = system.get_file_info(text) + if not path_stat or path_stat.type ~= 'dir' then + core.error("Cannot open folder %q", text) + return + end + if core.confirm_close_all() then + core.open_folder_project(text) + end + end, function(text) + return text == "" and core.recent_projects or common.dir_path_suggest(text) + end) + end, + + ["core:open-project-folder"] = function() + core.command_view:enter("Open Project", function(text) + local path_stat = system.get_file_info(text) + if not path_stat or path_stat.type ~= 'dir' then + core.error("Cannot open folder %q", text) + return + end + system.exec(string.format("%q %q", EXEFILE, text)) + end, function(text) + return text == "" and core.recent_projects or common.dir_path_suggest(text) + end) + end, }) diff --git a/data/core/common.lua b/data/core/common.lua index 1fc91b80..659cf244 100644 --- a/data/core/common.lua +++ b/data/core/common.lua @@ -102,6 +102,21 @@ function common.path_suggest(text) end +function common.dir_path_suggest(text) + local path, name = text:match("^(.-)([^/\\]*)$") + local files = system.list_dir(path == "" and "." or path) or {} + local res = {} + for _, file in ipairs(files) do + file = path .. file + local info = system.get_file_info(file) + if info and info.type == "dir" and file:lower():find(text:lower(), nil, true) == 1 then + table.insert(res, file) + end + end + return res +end + + function common.match_pattern(text, pattern, ...) if type(pattern) == "string" then return text:find(pattern, ...) diff --git a/data/core/docview.lua b/data/core/docview.lua index 73191c23..4bffd56f 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -187,8 +187,14 @@ function DocView:scroll_to_make_visible(line, col) self.scroll.to.y = math.max(self.scroll.to.y, max) local gw = self:get_gutter_width() local xoffset = self:get_col_x_offset(line, col) - local max = xoffset - self.size.x + gw + self.size.x / 5 - self.scroll.to.x = math.max(0, max) + local xmargin = 3 * self:get_font():get_width(' ') + local xsup = xoffset + gw + xmargin + local xinf = xoffset - xmargin + if xsup > self.scroll.x + self.size.x then + self.scroll.to.x = xsup - self.size.x + elseif xinf < self.scroll.x then + self.scroll.to.x = math.max(0, xinf) + end end diff --git a/data/core/init.lua b/data/core/init.lua index e50259b4..19852233 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -11,8 +11,53 @@ local Doc local core = {} +local function table_serialize(t) + local ls = {"{"} + for i = 1, #t do + ls[#ls + 1] = string.format(" %q,", t[i]) + end + ls[#ls + 1] = "}" + return table.concat(ls, "\n") +end + +local function load_projects() + local ok, t = pcall(dofile, USERDIR .. "/recent_projects.lua") + core.recent_projects = (ok and t or {}) +end + +local function add_project_to_recents(dirname) + dirname = system.absolute_path(dirname) + if not dirname then return end + local recents = core.recent_projects + local n = #recents + for i = 1, n do + if dirname == recents[i] then + table.remove(recents, i) + break + end + end + table.insert(recents, 1, dirname) +end + +local function save_projects() + local fp = io.open(USERDIR .. "/recent_projects.lua", "w") + if fp then + fp:write("return ", table_serialize(core.recent_projects), "\n") + fp:close() + end +end + +function core.open_folder_project(dirname) + core.root_view:close_all_docviews() + add_project_to_recents(dirname) + save_projects() + core.switch_project = dirname + core.threads[core.project_scan_thread_id].wake = 0 +end local function project_scan_thread() + local priority_run = true + local function diff_files(a, b) if #a ~= #b then return true end for i, v in ipairs(a) do @@ -28,7 +73,7 @@ local function project_scan_thread() end local function get_files(path, t) - coroutine.yield() + if not priority_run then coroutine.yield() end t = t or {} local size_limit = config.file_size_limit * 10e5 local all = system.list_dir(path) or {} @@ -78,10 +123,17 @@ local function project_scan_thread() end core.project_files = t core.redraw = true + priority_run = false end -- wait for next scan - coroutine.yield(config.project_scan_rate) + if core.switch_project then + system.chdir(core.switch_project) + priority_run = true + core.switch_project = nil + else + coroutine.yield(config.project_scan_rate) + end end end @@ -121,30 +173,54 @@ local function write_user_init_file(init_filename) init_file:write([[ -- put user settings here -- this module will be loaded after everything else when the application starts +-- it will be automatically reloaded when saved local keymap = require "core.keymap" local config = require "core.config" local style = require "core.style" -- light theme: --- require "colors.summer" +-- style.load("colors.summer") -- key binding: -- keymap.add { ["ctrl+escape"] = "core:quit" } + +-- customize fonts: +-- style.font = renderer.font.load(DATADIR .. "/fonts/font.ttf", 14 * SCALE) +-- style.code_font = renderer.font.load(DATADIR .. "/fonts/monospace.ttf", 13.5 * SCALE) +-- +-- fonts used by the editor: +-- style.font : user interface +-- style.big_font : big text in welcome screen +-- style.icon_font : icons +-- style.code_font : code +-- +-- the function to load the font accept a 3rd optional argument like: +-- +-- {antialiasing="grayscale", hinting="full"} +-- +-- possible values are: +-- antialiasing: grayscale, subpixel +-- hinting: none, slight, full ]]) init_file:close() end -local function load_user_directory() - local init_filename = USERDIR .. "/init.lua" - local info = system.get_file_info(USERDIR) - if not info then - create_user_directory() - write_user_init_file(init_filename) - end - return dofile(init_filename) +function core.load_user_directory() + return core.try(function() + local stat_dir = system.get_file_info(USERDIR) + if not stat_dir then + create_user_directory() + end + local init_filename = USERDIR .. "/init.lua" + local stat_file = system.get_file_info(init_filename) + if not stat_file then + write_user_init_file(init_filename) + end + dofile(init_filename) + end) end @@ -156,7 +232,9 @@ function core.init() CommandView = require "core.commandview" Doc = require "core.doc" - local project_dir = "." + load_projects() + + local project_dir = core.recent_projects[1] or "." local files = {} for i = 2, #ARGS do local info = system.get_file_info(ARGS[i]) or {} @@ -164,6 +242,8 @@ function core.init() table.insert(files, system.absolute_path(ARGS[i])) elseif info.type == "dir" then project_dir = ARGS[i] + add_project_to_recents(project_dir) + save_projects() end end @@ -177,18 +257,25 @@ function core.init() core.project_files = {} core.redraw = true core.visited_files = {} + core.restart_request = false core.root_view = RootView() core.command_view = CommandView() core.status_view = StatusView() + core.root_view.root_node.is_primary_view = true core.root_view.root_node:split("down", core.command_view, true) core.root_view.root_node.b:split("down", core.status_view, true) - core.add_thread(project_scan_thread) + core.project_scan_thread_id = core.add_thread(project_scan_thread) command.add_defaults() local got_plugin_error = not core.load_plugins() - local got_user_error = not core.try(load_user_directory) + local got_user_error = not core.load_user_directory() + + do + local pdir, pname = system.absolute_path(project_dir):match("(.*)[/\\\\](.*)") + core.log("Opening project %q from directory %q", pname, pdir) + end local got_project_error = not core.load_project_module() for _, filename in ipairs(files) do @@ -201,6 +288,28 @@ function core.init() end +function core.confirm_close_all() + local dirty_count = 0 + local dirty_name + for _, doc in ipairs(core.docs) do + if doc:is_dirty() then + dirty_count = dirty_count + 1 + dirty_name = doc:get_name() + end + end + if dirty_count > 0 then + local text + if dirty_count == 1 then + text = string.format("\"%s\" has unsaved changes. Quit anyway?", dirty_name) + else + text = string.format("%d docs have unsaved changes. Quit anyway?", dirty_count) + end + local confirm = system.show_confirm_dialog("Unsaved Changes", text) + if not confirm then return false end + end + return true +end + local temp_uid = (system.get_time() * 1000) % 0xffffffff local temp_file_prefix = string.format(".lite_temp_%08x", temp_uid) local temp_file_counter = 0 @@ -225,25 +334,16 @@ function core.quit(force) delete_temp_files() os.exit() end - local dirty_count = 0 - local dirty_name - for _, doc in ipairs(core.docs) do - if doc:is_dirty() then - dirty_count = dirty_count + 1 - dirty_name = doc:get_name() - end + if core.confirm_close_all() then + core.quit(true) end - if dirty_count > 0 then - local text - if dirty_count == 1 then - text = string.format("\"%s\" has unsaved changes. Quit anyway?", dirty_name) - else - text = string.format("%d docs have unsaved changes. Quit anyway?", dirty_count) - end - local confirm = system.show_confirm_dialog("Unsaved Changes", text) - if not confirm then return end +end + + +function core.restart() + if core.confirm_close_all() then + core.restart_request = true end - core.quit(true) end @@ -317,6 +417,7 @@ function core.add_thread(f, weak_ref) local key = weak_ref or #core.threads + 1 local fn = function() return core.try(f) end core.threads[key] = { cr = coroutine.create(fn), wake = 0 } + return key end @@ -544,10 +645,12 @@ end) function core.run() local idle_iterations = 0 + local frame_duration = 1 / config.fps while true do core.frame_start = system.get_time() local did_redraw = core.step() local need_more_work = run_threads() + if core.restart_request then break end if not did_redraw and not need_more_work then idle_iterations = idle_iterations + 1 -- do not wait of events at idle_iterations = 1 to give a chance at core.step to run @@ -555,16 +658,16 @@ function core.run() if idle_iterations > 1 then if system.window_has_focus() then -- keep running even with no events to make the cursor blinks - system.wait_event(1 / config.fps) + system.wait_event(frame_duration) else system.wait_event() end end else idle_iterations = 0 + local elapsed = system.get_time() - core.frame_start + system.sleep(math.max(0, frame_duration - elapsed)) end - local elapsed = system.get_time() - core.frame_start - system.sleep(math.max(0, 1 / config.fps - elapsed)) end end diff --git a/data/core/keymap.lua b/data/core/keymap.lua index 4d48ed4e..3798d4da 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -88,6 +88,8 @@ keymap.add { ["ctrl+p"] = "core:find-file", ["ctrl+o"] = "core:open-file", ["ctrl+n"] = "core:new-doc", + ["ctrl+shift+c"] = "core:change-project-folder", + ["ctrl+shift+o"] = "core:open-project-folder", ["alt+return"] = "core:toggle-fullscreen", ["alt+shift+j"] = "root:split-left", diff --git a/data/core/rootview.lua b/data/core/rootview.lua index 51150079..f311b11d 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -4,6 +4,7 @@ local style = require "core.style" local keymap = require "core.keymap" local Object = require "core.object" local View = require "core.view" +local CommandView = require "core.commandview" local DocView = require "core.docview" @@ -18,9 +19,11 @@ local function draw_text(x, y, color) local lines = { { fmt = "%s to run a command", cmd = "core:find-command" }, { fmt = "%s to open a file from the project", cmd = "core:find-file" }, + { fmt = "%s to change project folder", cmd = "core:change-project-folder" }, + { fmt = "%s to open a project folder", cmd = "core:open-project-folder" }, } th = style.font:get_height() - y = y + (dh - th * 2 - style.padding.y) / 2 + y = y + (dh - (th + style.padding.y) * #lines) / 2 local w = 0 for _, line in ipairs(lines) do local text = string.format(line.fmt, keymap.get_binding(line.cmd)) @@ -376,6 +379,41 @@ function Node:draw() end +function Node:is_empty() + if self.type == "leaf" then + return #self.views == 0 + else + return self.a:is_empty() and self.b:is_empty() + end +end + + +function Node:close_all_docviews() + if self.type == "leaf" then + local i = 1 + while i <= #self.views do + local view = self.views[i] + if view:is(DocView) and not view:is(CommandView) then + table.remove(self.views, i) + else + i = i + 1 + end + end + if #self.views == 0 and self.is_primary_view then + self:add_view(EmptyView()) + end + else + self.a:close_all_docviews() + self.b:close_all_docviews() + if self.a:is_empty() then + self:consume(self.b) + elseif self.b:is_empty() then + self:consume(self.a) + end + end +end + + local RootView = View:extend() @@ -396,20 +434,17 @@ function RootView:get_active_node() return self.root_node:get_node_for_view(core.active_view) end --- Get un unlocked node with at least one view. -local function get_node_unlocked(node) - if not node.locked and #node.views > 0 then +local function get_primary_view(node) + if node.is_primary_view then return node end if node.type ~= "leaf" then - local a = get_node_unlocked(node.a) - if a then return a end - return get_node_unlocked(node.b) + return get_primary_view(node.a) or get_primary_view(node.b) end end -function RootView:get_document_view() - local node = get_node_unlocked(self.root_node) +function RootView:get_primary_view() + local node = get_primary_view(self.root_node) if node then return node.views[1] end @@ -418,8 +453,8 @@ end function RootView:open_doc(doc) local node = self:get_active_node() if node.locked then - local default_view = self:get_document_view() - assert(default_view, "Cannot find an unlocked node to open the document.") + local default_view = self:get_primary_view() + assert(default_view, "internal error: cannot find original document node.") core.set_active_view(default_view) node = self:get_active_node() end @@ -437,6 +472,11 @@ function RootView:open_doc(doc) end +function RootView:close_all_docviews() + self.root_node:close_all_docviews() +end + + function RootView:on_mouse_pressed(button, x, y, clicks) local div = self.root_node:get_divider_overlapping_point(x, y) if div then diff --git a/data/core/style.lua b/data/core/style.lua index 1084540d..45492a6d 100644 --- a/data/core/style.lua +++ b/data/core/style.lua @@ -53,4 +53,10 @@ style.syntax["string"] = { common.color "#f7c95c" } style.syntax["operator"] = { common.color "#93DDFA" } style.syntax["function"] = { common.color "#93DDFA" } + +style.load = function(module_name) + package.loaded[module_name] = nil + require(module_name) +end + return style diff --git a/data/user/init.lua b/data/user/init.lua index bb7f4b3f..afaa6108 100644 --- a/data/user/init.lua +++ b/data/user/init.lua @@ -1,13 +1,31 @@ -- put user settings here -- this module will be loaded after everything else when the application starts +-- it will be automatically reloaded when saved local keymap = require "core.keymap" local config = require "core.config" local style = require "core.style" -- light theme: --- require "colors.summer" +-- style.load("colors.summer") -- key binding: -- keymap.add { ["ctrl+escape"] = "core:quit" } +-- customize fonts: +-- style.font = renderer.font.load(DATADIR .. "/fonts/font.ttf", 14 * SCALE) +-- style.code_font = renderer.font.load(DATADIR .. "/fonts/monospace.ttf", 13.5 * SCALE) +-- +-- fonts used by the editor: +-- style.font : user interface +-- style.big_font : big text in welcome screen +-- style.icon_font : icons +-- style.code_font : code +-- +-- the function to load the font accept a 3rd optional argument like: +-- +-- {antialiasing="grayscale", hinting="full"} +-- +-- possible values are: +-- antialiasing: grayscale, subpixel +-- hinting: none, slight, full diff --git a/doc/usage.md b/doc/usage.md index 5ea4e28e..7efcb4ff 100644 --- a/doc/usage.md +++ b/doc/usage.md @@ -140,6 +140,13 @@ then be loaded manually as needed by using the `require` function. Plugins can be downloaded from the [plugins repository](https://github.com/rxi/lite-plugins). +## Restarting the editor + +If you modifies the user configuration file or some of the Lua implementation files you may +restart the editor using the command "Core: Restart". +All the application will be restarting by keeping the window that is already used. + + ## Color Themes Colors themes in lite are lua modules which overwrite the color fields of lite's `core.style` module. diff --git a/lib/font_renderer/font_renderer_alpha.h b/lib/font_renderer/font_renderer_alpha.h index 5dcf5e37..53304688 100644 --- a/lib/font_renderer/font_renderer_alpha.h +++ b/lib/font_renderer/font_renderer_alpha.h @@ -81,20 +81,17 @@ public: void set_font_height(double height) { const double scale_x = (m_prescale_x ? 100.0 : 1.0); m_feng.height(height); - if (m_subpixel) { - const int subpixel_scale = 3; - m_feng.width(height * scale_x * subpixel_scale); - } else { - m_feng.width(height * scale_x); - } + m_feng.width(height * scale_x); } template void draw_codepoint(Rasterizer& ras, Scanline& sl, RenSolid& ren_solid, const color_type color, - int codepoint, double& x, double& y, int subpixel_scale) + int codepoint, double& x, double& y, const int subpixel_scale) { const double scale_x = (m_prescale_x ? 100.0 : 1.0); + // Coefficient to scale back the glyph to the final scale. + const double cx_inv_scale = subpixel_scale / scale_x; // Represent the delta in x scaled by scale_x. double x_delta = 0; @@ -114,15 +111,15 @@ public: double ty = m_hinting ? floor(y + 0.5) : y; ras.reset(); m_mtx.reset(); - m_mtx *= agg::trans_affine_scaling(1.0 / scale_x, 1); - m_mtx *= agg::trans_affine_translation(start_x + x_delta / scale_x, ty); + m_mtx *= agg::trans_affine_scaling(cx_inv_scale, 1); + m_mtx *= agg::trans_affine_translation(start_x + cx_inv_scale * x_delta, ty); ras.add_path(m_trans); ren_solid.color(color); agg::render_scanlines(ras, sl, ren_solid); } y += glyph->advance_y; - x += (x_delta + glyph->advance_x) / scale_x; + x += cx_inv_scale * (x_delta + glyph->advance_x); } } @@ -135,7 +132,7 @@ public: void render_codepoint(agg::rendering_buffer& ren_buf, const color_type text_color, double& x, double& y, - int codepoint, int subpixel_scale) + int codepoint, const int subpixel_scale) { if (!m_font_loaded) { return; diff --git a/run-local b/run-local index 17284818..1bf3cc2b 100755 --- a/run-local +++ b/run-local @@ -9,6 +9,8 @@ else datadir="$rundir/share/lite-xl" fi +userdir="$(realpath "$rundir")" + if [ "$#" -lt 1 ]; then echo "usage: $0 " exit 1 @@ -16,7 +18,7 @@ fi builddir="$1" rm -fr "$rundir" -mkdir -p "$bindir" "$datadir" +mkdir -p "$bindir" "$datadir" "$userdir" if [ -f "$builddir/src/lite" ]; then cp "$builddir/src/lite" "$bindir" elif [ -f "$builddir/src/lite.exe" ]; then @@ -28,4 +30,4 @@ fi for module_name in core plugins colors fonts; do cp -r "data/$module_name" "$datadir" done -exec "$bindir/lite" +HOME="$userdir" USERPROFILE="$userdir" exec "$bindir/lite" "${@:2}" diff --git a/src/main.c b/src/main.c index 770f9bfb..0bdd660d 100644 --- a/src/main.c +++ b/src/main.c @@ -89,8 +89,9 @@ int main(int argc, char **argv) { init_window_icon(); ren_init(window); - - lua_State *L = luaL_newstate(); + lua_State *L; +init_lua: + L = luaL_newstate(); luaL_openlibs(L); api_load_libs(L); @@ -117,7 +118,7 @@ int main(int argc, char **argv) { lua_setglobal(L, "EXEFILE"); - (void) luaL_dostring(L, + const char *init_lite_code = \ "local core\n" "xpcall(function()\n" " SCALE = tonumber(os.getenv(\"LITE_SCALE\")) or SCALE\n" @@ -153,8 +154,18 @@ int main(int argc, char **argv) { " pcall(core.on_error, err)\n" " end\n" " os.exit(1)\n" - "end)"); + "end)\n" + "return core and core.restart_request\n"; + if (luaL_loadstring(L, init_lite_code)) { + fprintf(stderr, "internal error when starting the application\n"); + exit(1); + } + lua_pcall(L, 0, 1, 0); + if (lua_toboolean(L, -1)) { + lua_close(L); + goto init_lua; + } lua_close(L); SDL_DestroyWindow(window);