├── .gitignore ├── data ├── fonts │ ├── font.ttf │ ├── icons.ttf │ └── monospace.ttf ├── user │ ├── init.lua │ └── colors │ │ ├── fall.lua │ │ └── summer.lua ├── core │ ├── strict.lua │ ├── config.lua │ ├── syntax.lua │ ├── commands │ │ ├── command.lua │ │ ├── core.lua │ │ ├── root.lua │ │ ├── findreplace.lua │ │ └── doc.lua │ ├── object.lua │ ├── doc │ │ ├── search.lua │ │ ├── highlighter.lua │ │ ├── translate.lua │ │ └── init.lua │ ├── command.lua │ ├── style.lua │ ├── logview.lua │ ├── tokenizer.lua │ ├── common.lua │ ├── view.lua │ ├── statusview.lua │ ├── keymap.lua │ ├── commandview.lua │ ├── docview.lua │ └── init.lua └── plugins │ ├── quote.lua │ ├── language_xml.lua │ ├── language_md.lua │ ├── trimwhitespace.lua │ ├── language_css.lua │ ├── autoreload.lua │ ├── macro.lua │ ├── reflow.lua │ ├── tabularize.lua │ ├── language_lua.lua │ ├── language_python.lua │ ├── language_c.lua │ ├── language_js.lua │ ├── language_odin.lua │ ├── treeview.lua │ ├── autocomplete.lua │ └── projectsearch.lua ├── README.md ├── api.odin ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── api_renderer_font.odin ├── api_renderer.odin ├── main.zig ├── main.odin ├── rencache.odin ├── renderer.odin └── api_system.odin /.gitignore: -------------------------------------------------------------------------------- 1 | liteodin 2 | -------------------------------------------------------------------------------- /data/fonts/font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Waqar144/lite-odin/HEAD/data/fonts/font.ttf -------------------------------------------------------------------------------- /data/fonts/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Waqar144/lite-odin/HEAD/data/fonts/icons.ttf -------------------------------------------------------------------------------- /data/fonts/monospace.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Waqar144/lite-odin/HEAD/data/fonts/monospace.ttf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Intro 2 | 3 | This is a port of https://github.com/rxi/lite/ to Odin which I did while learning Odin language. It should be more or less the same as the original editor, the plugins should work (though I haven't really tried them yet). It uses lua 5.4 instead of 5.2 4 | 5 | Included is a main.zig file which I did for comparison of both languages. 6 | -------------------------------------------------------------------------------- /data/user/init.lua: -------------------------------------------------------------------------------- 1 | -- put user settings here 2 | -- this module will be loaded after everything else when the application starts 3 | 4 | local keymap = require "core.keymap" 5 | local config = require "core.config" 6 | local style = require "core.style" 7 | 8 | -- light theme: 9 | -- require "user.colors.summer" 10 | 11 | -- key binding: 12 | -- keymap.add { ["ctrl+escape"] = "core:quit" } 13 | 14 | -------------------------------------------------------------------------------- /api.odin: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import lua "vendor:lua/5.4" 4 | 5 | API_TYPE_FONT :: "Font" 6 | 7 | // odinfmt: disable 8 | @(private="file") 9 | libs := [?]lua.L_Reg { 10 | { "system", luaopen_system }, 11 | { "renderer", luaopen_renderer }, 12 | } 13 | // odinfmt: enable 14 | 15 | api_load_libs :: proc(L: ^lua.State) { 16 | for lib in libs { 17 | lua.L_requiref(L, lib.name, lib.func, 1) 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /data/core/strict.lua: -------------------------------------------------------------------------------- 1 | local strict = {} 2 | strict.defined = {} 3 | 4 | 5 | -- used to define a global variable 6 | function global(t) 7 | for k, v in pairs(t) do 8 | strict.defined[k] = true 9 | rawset(_G, k, v) 10 | end 11 | end 12 | 13 | 14 | function strict.__newindex(t, k, v) 15 | error("cannot set undefined variable: " .. k, 2) 16 | end 17 | 18 | 19 | function strict.__index(t, k) 20 | if not strict.defined[k] then 21 | error("cannot get undefined variable: " .. k, 2) 22 | end 23 | end 24 | 25 | 26 | setmetatable(_G, strict) 27 | -------------------------------------------------------------------------------- /data/core/config.lua: -------------------------------------------------------------------------------- 1 | local config = {} 2 | 3 | config.project_scan_rate = 5 4 | config.fps = 60 5 | config.max_log_items = 80 6 | config.message_timeout = 3 7 | config.mouse_wheel_scroll = 50 * SCALE 8 | config.file_size_limit = 10 9 | config.ignore_files = "^%." 10 | config.symbol_pattern = "[%a_][%w_]*" 11 | config.non_word_chars = " \t\n/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-" 12 | config.undo_merge_timeout = 0.3 13 | config.max_undos = 10000 14 | config.highlight_current_line = true 15 | config.line_height = 1.2 16 | config.indent_size = 2 17 | config.tab_type = "soft" 18 | config.line_limit = 80 19 | 20 | return config 21 | -------------------------------------------------------------------------------- /data/core/syntax.lua: -------------------------------------------------------------------------------- 1 | local common = require "core.common" 2 | 3 | local syntax = {} 4 | syntax.items = {} 5 | 6 | local plain_text_syntax = { patterns = {}, symbols = {} } 7 | 8 | 9 | function syntax.add(t) 10 | table.insert(syntax.items, t) 11 | end 12 | 13 | 14 | local function find(string, field) 15 | for i = #syntax.items, 1, -1 do 16 | local t = syntax.items[i] 17 | if common.match_pattern(string, t[field] or {}) then 18 | return t 19 | end 20 | end 21 | end 22 | 23 | function syntax.get(filename, header) 24 | return find(filename, "files") 25 | or find(header, "headers") 26 | or plain_text_syntax 27 | end 28 | 29 | 30 | return syntax 31 | -------------------------------------------------------------------------------- /data/plugins/quote.lua: -------------------------------------------------------------------------------- 1 | local core = require "core" 2 | local command = require "core.command" 3 | local keymap = require "core.keymap" 4 | 5 | 6 | local escapes = { 7 | ["\\"] = "\\\\", 8 | ["\""] = "\\\"", 9 | ["\n"] = "\\n", 10 | ["\r"] = "\\r", 11 | ["\t"] = "\\t", 12 | ["\b"] = "\\b", 13 | } 14 | 15 | local function replace(chr) 16 | return escapes[chr] or string.format("\\x%02x", chr:byte()) 17 | end 18 | 19 | 20 | command.add("core.docview", { 21 | ["quote:quote"] = function() 22 | core.active_view.doc:replace(function(text) 23 | return '"' .. text:gsub("[\0-\31\\\"]", replace) .. '"' 24 | end) 25 | end, 26 | }) 27 | 28 | keymap.add { 29 | ["ctrl+'"] = "quote:quote", 30 | } 31 | -------------------------------------------------------------------------------- /data/core/commands/command.lua: -------------------------------------------------------------------------------- 1 | local core = require "core" 2 | local command = require "core.command" 3 | local CommandView = require "core.commandview" 4 | 5 | local function has_commandview() 6 | return core.active_view:is(CommandView) 7 | end 8 | 9 | 10 | command.add(has_commandview, { 11 | ["command:submit"] = function() 12 | core.active_view:submit() 13 | end, 14 | 15 | ["command:complete"] = function() 16 | core.active_view:complete() 17 | end, 18 | 19 | ["command:escape"] = function() 20 | core.active_view:exit() 21 | end, 22 | 23 | ["command:select-previous"] = function() 24 | core.active_view:move_suggestion_idx(1) 25 | end, 26 | 27 | ["command:select-next"] = function() 28 | core.active_view:move_suggestion_idx(-1) 29 | end, 30 | }) 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | compile: 5 | strategy: 6 | matrix: 7 | os: [ubuntu-latest, macos-latest, windows-latest] 8 | runs-on: ${{ matrix.os }} 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: laytan/setup-odin@v2 12 | with: 13 | token: ${{ secrets.GITHUB_TOKEN }} 14 | 15 | - name: Install Dependencies on Mac 16 | if: ${{ runner.os == 'macOS' }} 17 | run: brew install lua sdl2 18 | 19 | - name: Install Dependencies on Linux 20 | if: ${{ runner.os == 'Linux' }} 21 | run: | 22 | sudo apt update 23 | sudo apt install liblua5.4-dev libsdl2-dev 24 | make -j4 -C $(odin root)/vendor/stb/src 25 | 26 | - run: odin build . -vet -strict-style 27 | -------------------------------------------------------------------------------- /data/plugins/language_xml.lua: -------------------------------------------------------------------------------- 1 | local syntax = require "core.syntax" 2 | 3 | syntax.add { 4 | files = { "%.xml$", "%.html?$" }, 5 | headers = "<%?xml", 6 | patterns = { 7 | { pattern = { "" }, type = "comment" }, 8 | { pattern = { '%f[^>][^<]', '%f[<]' }, type = "normal" }, 9 | { pattern = { '"', '"', '\\' }, type = "string" }, 10 | { pattern = { "'", "'", '\\' }, type = "string" }, 11 | { pattern = "0x[%da-fA-F]+", type = "number" }, 12 | { pattern = "-?%d+[%d%.]*f?", type = "number" }, 13 | { pattern = "-?%.?%d+f?", type = "number" }, 14 | { pattern = "%f[^<]![%a_][%w_]*", type = "keyword2" }, 15 | { pattern = "%f[^<][%a_][%w_]*", type = "function" }, 16 | { pattern = "%f[^<]/[%a_][%w_]*", type = "function" }, 17 | { pattern = "[%a_][%w_]*", type = "keyword" }, 18 | { pattern = "[/<>=]", type = "operator" }, 19 | }, 20 | symbols = {}, 21 | } 22 | -------------------------------------------------------------------------------- /data/plugins/language_md.lua: -------------------------------------------------------------------------------- 1 | local syntax = require "core.syntax" 2 | 3 | syntax.add { 4 | files = { "%.md$", "%.markdown$" }, 5 | patterns = { 6 | { pattern = "\\.", type = "normal" }, 7 | { pattern = { "" }, type = "comment" }, 8 | { pattern = { "```", "```" }, type = "string" }, 9 | { pattern = { "``", "``", "\\" }, type = "string" }, 10 | { pattern = { "`", "`", "\\" }, type = "string" }, 11 | { pattern = { "~~", "~~", "\\" }, type = "keyword2" }, 12 | { pattern = "%-%-%-+", type = "comment" }, 13 | { pattern = "%*%s+", type = "operator" }, 14 | { pattern = { "%*", "[%*\n]", "\\" }, type = "operator" }, 15 | { pattern = { "%_", "[%_\n]", "\\" }, type = "keyword2" }, 16 | { pattern = "#.-\n", type = "keyword" }, 17 | { pattern = "!?%[.-%]%(.-%)", type = "function" }, 18 | { pattern = "https?://%S+", type = "function" }, 19 | }, 20 | symbols = { }, 21 | } 22 | -------------------------------------------------------------------------------- /data/plugins/trimwhitespace.lua: -------------------------------------------------------------------------------- 1 | local core = require "core" 2 | local command = require "core.command" 3 | local Doc = require "core.doc" 4 | 5 | 6 | local function trim_trailing_whitespace(doc) 7 | local cline, ccol = doc:get_selection() 8 | for i = 1, #doc.lines do 9 | local old_text = doc:get_text(i, 1, i, math.huge) 10 | local new_text = old_text:gsub("%s*$", "") 11 | 12 | -- don't remove whitespace which would cause the caret to reposition 13 | if cline == i and ccol > #new_text then 14 | new_text = old_text:sub(1, ccol - 1) 15 | end 16 | 17 | if old_text ~= new_text then 18 | doc:insert(i, 1, new_text) 19 | doc:remove(i, #new_text + 1, i, math.huge) 20 | end 21 | end 22 | end 23 | 24 | 25 | command.add("core.docview", { 26 | ["trim-whitespace:trim-trailing-whitespace"] = function() 27 | trim_trailing_whitespace(core.active_view.doc) 28 | end, 29 | }) 30 | 31 | 32 | local save = Doc.save 33 | Doc.save = function(self, ...) 34 | trim_trailing_whitespace(self) 35 | save(self, ...) 36 | end 37 | -------------------------------------------------------------------------------- /data/core/object.lua: -------------------------------------------------------------------------------- 1 | local Object = {} 2 | Object.__index = Object 3 | 4 | 5 | function Object:new() 6 | end 7 | 8 | 9 | function Object:extend() 10 | local cls = {} 11 | for k, v in pairs(self) do 12 | if k:find("__") == 1 then 13 | cls[k] = v 14 | end 15 | end 16 | cls.__index = cls 17 | cls.super = self 18 | setmetatable(cls, self) 19 | return cls 20 | end 21 | 22 | 23 | function Object:implement(...) 24 | for _, cls in pairs({...}) do 25 | for k, v in pairs(cls) do 26 | if self[k] == nil and type(v) == "function" then 27 | self[k] = v 28 | end 29 | end 30 | end 31 | end 32 | 33 | 34 | function Object:is(T) 35 | local mt = getmetatable(self) 36 | while mt do 37 | if mt == T then 38 | return true 39 | end 40 | mt = getmetatable(mt) 41 | end 42 | return false 43 | end 44 | 45 | 46 | function Object:__tostring() 47 | return "Object" 48 | end 49 | 50 | 51 | function Object:__call(...) 52 | local obj = setmetatable({}, self) 53 | obj:new(...) 54 | return obj 55 | end 56 | 57 | 58 | return Object 59 | -------------------------------------------------------------------------------- /data/plugins/language_css.lua: -------------------------------------------------------------------------------- 1 | local syntax = require "core.syntax" 2 | 3 | syntax.add { 4 | files = { "%.css$" }, 5 | patterns = { 6 | { pattern = "\\.", type = "normal" }, 7 | { pattern = "//.-\n", type = "comment" }, 8 | { pattern = { "/%*", "%*/" }, type = "comment" }, 9 | { pattern = { '"', '"', '\\' }, type = "string" }, 10 | { pattern = { "'", "'", '\\' }, type = "string" }, 11 | { pattern = "[%a][%w-]*%s*%f[:]", type = "keyword" }, 12 | { pattern = "#%x+", type = "string" }, 13 | { pattern = "-?%d+[%d%.]*p[xt]", type = "number" }, 14 | { pattern = "-?%d+[%d%.]*deg", type = "number" }, 15 | { pattern = "-?%d+[%d%.]*", type = "number" }, 16 | { pattern = "[%a_][%w_]*", type = "symbol" }, 17 | { pattern = "#[%a][%w_-]*", type = "keyword2" }, 18 | { pattern = "@[%a][%w_-]*", type = "keyword2" }, 19 | { pattern = "%.[%a][%w_-]*", type = "keyword2" }, 20 | { pattern = "[{}:]", type = "operator" }, 21 | }, 22 | symbols = {}, 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 rxi 2 | Copyright (c) 2024 Waqar Ahmed 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /data/core/doc/search.lua: -------------------------------------------------------------------------------- 1 | local search = {} 2 | 3 | local default_opt = {} 4 | 5 | 6 | local function pattern_lower(str) 7 | if str:sub(1, 1) == "%" then 8 | return str 9 | end 10 | return str:lower() 11 | end 12 | 13 | 14 | local function init_args(doc, line, col, text, opt) 15 | opt = opt or default_opt 16 | line, col = doc:sanitize_position(line, col) 17 | 18 | if opt.no_case then 19 | if opt.pattern then 20 | text = text:gsub("%%?.", pattern_lower) 21 | else 22 | text = text:lower() 23 | end 24 | end 25 | 26 | return doc, line, col, text, opt 27 | end 28 | 29 | 30 | function search.find(doc, line, col, text, opt) 31 | doc, line, col, text, opt = init_args(doc, line, col, text, opt) 32 | 33 | for line = line, #doc.lines do 34 | local line_text = doc.lines[line] 35 | if opt.no_case then 36 | line_text = line_text:lower() 37 | end 38 | local s, e = line_text:find(text, col, not opt.pattern) 39 | if s then 40 | return line, s, line, e + 1 41 | end 42 | col = 1 43 | end 44 | 45 | if opt.wrap then 46 | opt = { no_case = opt.no_case, pattern = opt.pattern } 47 | return search.find(doc, 1, 1, text, opt) 48 | end 49 | end 50 | 51 | 52 | return search 53 | -------------------------------------------------------------------------------- /data/user/colors/fall.lua: -------------------------------------------------------------------------------- 1 | local style = require "core.style" 2 | local common = require "core.common" 3 | 4 | style.background = { common.color "#343233" } 5 | style.background2 = { common.color "#2c2a2b" } 6 | style.background3 = { common.color "#2c2a2b" } 7 | style.text = { common.color "#c4b398" } 8 | style.caret = { common.color "#61efce" } 9 | style.accent = { common.color "#ffd152" } 10 | style.dim = { common.color "#615d5f" } 11 | style.divider = { common.color "#242223" } 12 | style.selection = { common.color "#454244" } 13 | style.line_number = { common.color "#454244" } 14 | style.line_number2 = { common.color "#615d5f" } 15 | style.line_highlight = { common.color "#383637" } 16 | style.scrollbar = { common.color "#454344" } 17 | style.scrollbar2 = { common.color "#524F50" } 18 | 19 | style.syntax["normal"] = { common.color "#efdab9" } 20 | style.syntax["symbol"] = { common.color "#efdab9" } 21 | style.syntax["comment"] = { common.color "#615d5f" } 22 | style.syntax["keyword"] = { common.color "#d36e2d" } 23 | style.syntax["keyword2"] = { common.color "#ef6179" } 24 | style.syntax["number"] = { common.color "#ffd152" } 25 | style.syntax["literal"] = { common.color "#ffd152" } 26 | style.syntax["string"] = { common.color "#ffd152" } 27 | style.syntax["operator"] = { common.color "#efdab9" } 28 | style.syntax["function"] = { common.color "#61efce" } 29 | -------------------------------------------------------------------------------- /data/user/colors/summer.lua: -------------------------------------------------------------------------------- 1 | local style = require "core.style" 2 | local common = require "core.common" 3 | 4 | style.background = { common.color "#fbfbfb" } 5 | style.background2 = { common.color "#f2f2f2" } 6 | style.background3 = { common.color "#f2f2f2" } 7 | style.text = { common.color "#404040" } 8 | style.caret = { common.color "#fc1785" } 9 | style.accent = { common.color "#fc1785" } 10 | style.dim = { common.color "#b0b0b0" } 11 | style.divider = { common.color "#e8e8e8" } 12 | style.selection = { common.color "#b7dce8" } 13 | style.line_number = { common.color "#d0d0d0" } 14 | style.line_number2 = { common.color "#808080" } 15 | style.line_highlight = { common.color "#f2f2f2" } 16 | style.scrollbar = { common.color "#e0e0e0" } 17 | style.scrollbar2 = { common.color "#c0c0c0" } 18 | 19 | style.syntax["normal"] = { common.color "#181818" } 20 | style.syntax["symbol"] = { common.color "#181818" } 21 | style.syntax["comment"] = { common.color "#22a21f" } 22 | style.syntax["keyword"] = { common.color "#fb6620" } 23 | style.syntax["keyword2"] = { common.color "#fc1785" } 24 | style.syntax["number"] = { common.color "#1586d2" } 25 | style.syntax["literal"] = { common.color "#1586d2" } 26 | style.syntax["string"] = { common.color "#1586d2" } 27 | style.syntax["operator"] = { common.color "#fb6620" } 28 | style.syntax["function"] = { common.color "#fc1785" } 29 | -------------------------------------------------------------------------------- /data/plugins/autoreload.lua: -------------------------------------------------------------------------------- 1 | local core = require "core" 2 | local config = require "core.config" 3 | local Doc = require "core.doc" 4 | 5 | 6 | local times = setmetatable({}, { __mode = "k" }) 7 | 8 | local function update_time(doc) 9 | local info = system.get_file_info(doc.filename) 10 | times[doc] = info.modified 11 | end 12 | 13 | 14 | local function reload_doc(doc) 15 | local fp = io.open(doc.filename, "r") 16 | local text = fp:read("*a") 17 | fp:close() 18 | 19 | local sel = { doc:get_selection() } 20 | doc:remove(1, 1, math.huge, math.huge) 21 | doc:insert(1, 1, text:gsub("\r", ""):gsub("\n$", "")) 22 | doc:set_selection(table.unpack(sel)) 23 | 24 | update_time(doc) 25 | doc:clean() 26 | core.log_quiet("Auto-reloaded doc \"%s\"", doc.filename) 27 | end 28 | 29 | 30 | core.add_thread(function() 31 | while true do 32 | -- check all doc modified times 33 | for _, doc in ipairs(core.docs) do 34 | local info = system.get_file_info(doc.filename or "") 35 | if info and times[doc] ~= info.modified then 36 | reload_doc(doc) 37 | end 38 | coroutine.yield() 39 | end 40 | 41 | -- wait for next scan 42 | coroutine.yield(config.project_scan_rate) 43 | end 44 | end) 45 | 46 | 47 | -- patch `Doc.save|load` to store modified time 48 | local load = Doc.load 49 | local save = Doc.save 50 | 51 | Doc.load = function(self, ...) 52 | local res = load(self, ...) 53 | update_time(self) 54 | return res 55 | end 56 | 57 | Doc.save = function(self, ...) 58 | local res = save(self, ...) 59 | update_time(self) 60 | return res 61 | end 62 | -------------------------------------------------------------------------------- /data/core/command.lua: -------------------------------------------------------------------------------- 1 | local core = require "core" 2 | local command = {} 3 | 4 | command.map = {} 5 | 6 | local always_true = function() return true end 7 | 8 | 9 | function command.add(predicate, map) 10 | predicate = predicate or always_true 11 | if type(predicate) == "string" then 12 | predicate = require(predicate) 13 | end 14 | if type(predicate) == "table" then 15 | local class = predicate 16 | predicate = function() return core.active_view:is(class) end 17 | end 18 | for name, fn in pairs(map) do 19 | assert(not command.map[name], "command already exists: " .. name) 20 | command.map[name] = { predicate = predicate, perform = fn } 21 | end 22 | end 23 | 24 | 25 | local function capitalize_first(str) 26 | return str:sub(1, 1):upper() .. str:sub(2) 27 | end 28 | 29 | function command.prettify_name(name) 30 | return name:gsub(":", ": "):gsub("-", " "):gsub("%S+", capitalize_first) 31 | end 32 | 33 | 34 | function command.get_all_valid() 35 | local res = {} 36 | for name, cmd in pairs(command.map) do 37 | if cmd.predicate() then 38 | table.insert(res, name) 39 | end 40 | end 41 | return res 42 | end 43 | 44 | 45 | local function perform(name) 46 | local cmd = command.map[name] 47 | if cmd and cmd.predicate() then 48 | cmd.perform() 49 | return true 50 | end 51 | return false 52 | end 53 | 54 | 55 | function command.perform(...) 56 | local ok, res = core.try(perform, ...) 57 | return not ok or res 58 | end 59 | 60 | 61 | function command.add_defaults() 62 | local reg = { "core", "root", "command", "doc", "findreplace" } 63 | for _, name in ipairs(reg) do 64 | require("core.commands." .. name) 65 | end 66 | end 67 | 68 | 69 | return command 70 | -------------------------------------------------------------------------------- /data/plugins/macro.lua: -------------------------------------------------------------------------------- 1 | local core = require "core" 2 | local command = require "core.command" 3 | local keymap = require "core.keymap" 4 | 5 | local handled_events = { 6 | ["keypressed"] = true, 7 | ["keyreleased"] = true, 8 | ["textinput"] = true, 9 | } 10 | 11 | local state = "stopped" 12 | local event_buffer = {} 13 | local modkeys = {} 14 | 15 | local on_event = core.on_event 16 | 17 | core.on_event = function(type, ...) 18 | local res = on_event(type, ...) 19 | if state == "recording" and handled_events[type] then 20 | table.insert(event_buffer, { type, ... }) 21 | end 22 | return res 23 | end 24 | 25 | 26 | local function clone(t) 27 | local res = {} 28 | for k, v in pairs(t) do res[k] = v end 29 | return res 30 | end 31 | 32 | 33 | local function predicate() 34 | return state ~= "playing" 35 | end 36 | 37 | 38 | command.add(predicate, { 39 | ["macro:toggle-record"] = function() 40 | if state == "stopped" then 41 | state = "recording" 42 | event_buffer = {} 43 | modkeys = clone(keymap.modkeys) 44 | core.log("Recording macro...") 45 | else 46 | state = "stopped" 47 | core.log("Stopped recording macro (%d events)", #event_buffer) 48 | end 49 | end, 50 | 51 | ["macro:play"] = function() 52 | state = "playing" 53 | core.log("Playing macro... (%d events)", #event_buffer) 54 | local mk = keymap.modkeys 55 | keymap.modkeys = clone(modkeys) 56 | for _, ev in ipairs(event_buffer) do 57 | on_event(table.unpack(ev)) 58 | core.root_view:update() 59 | end 60 | keymap.modkeys = mk 61 | state = "stopped" 62 | end, 63 | }) 64 | 65 | 66 | keymap.add { 67 | ["ctrl+shift+;"] = "macro:toggle-record", 68 | ["ctrl+;"] = "macro:play", 69 | } 70 | -------------------------------------------------------------------------------- /data/plugins/reflow.lua: -------------------------------------------------------------------------------- 1 | local core = require "core" 2 | local config = require "core.config" 3 | local command = require "core.command" 4 | local keymap = require "core.keymap" 5 | 6 | 7 | local function wordwrap_text(text, limit) 8 | local t = {} 9 | local n = 0 10 | 11 | for word in text:gmatch("%S+") do 12 | if n + #word > limit then 13 | table.insert(t, "\n") 14 | n = 0 15 | elseif #t > 0 then 16 | table.insert(t, " ") 17 | end 18 | table.insert(t, word) 19 | n = n + #word + 1 20 | end 21 | 22 | return table.concat(t) 23 | end 24 | 25 | 26 | command.add("core.docview", { 27 | ["reflow:reflow"] = function() 28 | local doc = core.active_view.doc 29 | doc:replace(function(text) 30 | local prefix_set = "[^%w\n%[%](){}`'\"]*" 31 | 32 | -- get line prefix and trailing whitespace 33 | local prefix1 = text:match("^\n*" .. prefix_set) 34 | local prefix2 = text:match("\n(" .. prefix_set .. ")", #prefix1+1) 35 | local trailing = text:match("%s*$") 36 | if not prefix2 or prefix2 == "" then 37 | prefix2 = prefix1 38 | end 39 | 40 | -- strip all line prefixes and trailing whitespace 41 | text = text:sub(#prefix1+1, -#trailing - 1):gsub("\n" .. prefix_set, "\n") 42 | 43 | -- split into blocks, wordwrap and join 44 | local line_limit = config.line_limit - #prefix1 45 | local blocks = {} 46 | text = text:gsub("\n\n", "\0") 47 | for block in text:gmatch("%Z+") do 48 | table.insert(blocks, wordwrap_text(block, line_limit)) 49 | end 50 | text = table.concat(blocks, "\n\n") 51 | 52 | -- add prefix to start of lines 53 | text = prefix1 .. text:gsub("\n", "\n" .. prefix2) .. trailing 54 | 55 | return text 56 | end) 57 | end, 58 | }) 59 | 60 | 61 | keymap.add { 62 | ["ctrl+shift+q"] = "reflow:reflow" 63 | } 64 | -------------------------------------------------------------------------------- /data/plugins/tabularize.lua: -------------------------------------------------------------------------------- 1 | local core = require "core" 2 | local command = require "core.command" 3 | local translate = require "core.doc.translate" 4 | 5 | 6 | local function gmatch_to_array(text, ptn) 7 | local res = {} 8 | for x in text:gmatch(ptn) do 9 | table.insert(res, x) 10 | end 11 | return res 12 | end 13 | 14 | 15 | local function tabularize_lines(lines, delim) 16 | local rows = {} 17 | local cols = {} 18 | 19 | -- split lines at delimiters and get maximum width of columns 20 | local ptn = "[^" .. delim:sub(1,1):gsub("%W", "%%%1") .. "]+" 21 | for i, line in ipairs(lines) do 22 | rows[i] = gmatch_to_array(line, ptn) 23 | for j, col in ipairs(rows[i]) do 24 | cols[j] = math.max(#col, cols[j] or 0) 25 | end 26 | end 27 | 28 | -- pad columns with space 29 | for _, row in ipairs(rows) do 30 | for i = 1, #row - 1 do 31 | row[i] = row[i] .. string.rep(" ", cols[i] - #row[i]) 32 | end 33 | end 34 | 35 | -- write columns back to lines array 36 | for i, line in ipairs(lines) do 37 | lines[i] = table.concat(rows[i], delim) 38 | end 39 | end 40 | 41 | 42 | command.add("core.docview", { 43 | ["tabularize:tabularize"] = function() 44 | core.command_view:enter("Tabularize On Delimiter", function(delim) 45 | if delim == "" then delim = " " end 46 | 47 | local doc = core.active_view.doc 48 | local line1, col1, line2, col2, swap = doc:get_selection(true) 49 | line1, col1 = doc:position_offset(line1, col1, translate.start_of_line) 50 | line2, col2 = doc:position_offset(line2, col2, translate.end_of_line) 51 | doc:set_selection(line1, col1, line2, col2, swap) 52 | 53 | doc:replace(function(text) 54 | local lines = gmatch_to_array(text, "[^\n]*\n?") 55 | tabularize_lines(lines, delim) 56 | return table.concat(lines) 57 | end) 58 | end) 59 | end, 60 | }) 61 | -------------------------------------------------------------------------------- /data/core/style.lua: -------------------------------------------------------------------------------- 1 | local common = require "core.common" 2 | local style = {} 3 | 4 | style.padding = { x = common.round(14 * SCALE), y = common.round(7 * SCALE) } 5 | style.divider_size = common.round(1 * SCALE) 6 | style.scrollbar_size = common.round(4 * SCALE) 7 | style.caret_width = common.round(2 * SCALE) 8 | style.tab_width = common.round(170 * SCALE) 9 | 10 | style.font = renderer.font.load(EXEDIR .. "/data/fonts/font.ttf", 14 * SCALE) 11 | style.big_font = renderer.font.load(EXEDIR .. "/data/fonts/font.ttf", 34 * SCALE) 12 | style.icon_font = renderer.font.load(EXEDIR .. "/data/fonts/icons.ttf", 14 * SCALE) 13 | style.code_font = renderer.font.load(EXEDIR .. "/data/fonts/monospace.ttf", 13.5 * SCALE) 14 | 15 | style.background = { common.color "#2e2e32" } 16 | style.background2 = { common.color "#252529" } 17 | style.background3 = { common.color "#252529" } 18 | style.text = { common.color "#97979c" } 19 | style.caret = { common.color "#93DDFA" } 20 | style.accent = { common.color "#e1e1e6" } 21 | style.dim = { common.color "#525257" } 22 | style.divider = { common.color "#202024" } 23 | style.selection = { common.color "#48484f" } 24 | style.line_number = { common.color "#525259" } 25 | style.line_number2 = { common.color "#83838f" } 26 | style.line_highlight = { common.color "#343438" } 27 | style.scrollbar = { common.color "#414146" } 28 | style.scrollbar2 = { common.color "#4b4b52" } 29 | 30 | style.syntax = {} 31 | style.syntax["normal"] = { common.color "#e1e1e6" } 32 | style.syntax["symbol"] = { common.color "#e1e1e6" } 33 | style.syntax["comment"] = { common.color "#676b6f" } 34 | style.syntax["keyword"] = { common.color "#E58AC9" } 35 | style.syntax["keyword2"] = { common.color "#F77483" } 36 | style.syntax["number"] = { common.color "#FFA94D" } 37 | style.syntax["literal"] = { common.color "#FFA94D" } 38 | style.syntax["string"] = { common.color "#f7c95c" } 39 | style.syntax["operator"] = { common.color "#93DDFA" } 40 | style.syntax["function"] = { common.color "#93DDFA" } 41 | 42 | return style 43 | -------------------------------------------------------------------------------- /data/plugins/language_lua.lua: -------------------------------------------------------------------------------- 1 | local syntax = require "core.syntax" 2 | 3 | syntax.add { 4 | files = "%.lua$", 5 | headers = "^#!.*[ /]lua", 6 | comment = "--", 7 | patterns = { 8 | { pattern = { '"', '"', '\\' }, type = "string" }, 9 | { pattern = { "'", "'", '\\' }, type = "string" }, 10 | { pattern = { "%[%[", "%]%]" }, type = "string" }, 11 | { pattern = { "%-%-%[%[", "%]%]"}, type = "comment" }, 12 | { pattern = "%-%-.-\n", type = "comment" }, 13 | { pattern = "-?0x%x+", type = "number" }, 14 | { pattern = "-?%d+[%d%.eE]*", type = "number" }, 15 | { pattern = "-?%.?%d+", type = "number" }, 16 | { pattern = "<%a+>", type = "keyword2" }, 17 | { pattern = "%.%.%.?", type = "operator" }, 18 | { pattern = "[<>~=]=", type = "operator" }, 19 | { pattern = "[%+%-=/%*%^%%#<>]", type = "operator" }, 20 | { pattern = "[%a_][%w_]*%s*%f[(\"{]", type = "function" }, 21 | { pattern = "[%a_][%w_]*", type = "symbol" }, 22 | { pattern = "::[%a_][%w_]*::", type = "function" }, 23 | }, 24 | symbols = { 25 | ["if"] = "keyword", 26 | ["then"] = "keyword", 27 | ["else"] = "keyword", 28 | ["elseif"] = "keyword", 29 | ["end"] = "keyword", 30 | ["do"] = "keyword", 31 | ["function"] = "keyword", 32 | ["repeat"] = "keyword", 33 | ["until"] = "keyword", 34 | ["while"] = "keyword", 35 | ["for"] = "keyword", 36 | ["break"] = "keyword", 37 | ["return"] = "keyword", 38 | ["local"] = "keyword", 39 | ["in"] = "keyword", 40 | ["not"] = "keyword", 41 | ["and"] = "keyword", 42 | ["or"] = "keyword", 43 | ["goto"] = "keyword", 44 | ["self"] = "keyword2", 45 | ["true"] = "literal", 46 | ["false"] = "literal", 47 | ["nil"] = "literal", 48 | }, 49 | } 50 | 51 | -------------------------------------------------------------------------------- /data/core/logview.lua: -------------------------------------------------------------------------------- 1 | local core = require "core" 2 | local style = require "core.style" 3 | local View = require "core.view" 4 | 5 | 6 | local LogView = View:extend() 7 | 8 | 9 | function LogView:new() 10 | LogView.super.new(self) 11 | self.last_item = core.log_items[#core.log_items] 12 | self.scrollable = true 13 | self.yoffset = 0 14 | end 15 | 16 | 17 | function LogView:get_name() 18 | return "Log" 19 | end 20 | 21 | 22 | function LogView:update() 23 | local item = core.log_items[#core.log_items] 24 | if self.last_item ~= item then 25 | self.last_item = item 26 | self.scroll.to.y = 0 27 | self.yoffset = -(style.font:get_height() + style.padding.y) 28 | end 29 | 30 | self:move_towards("yoffset", 0) 31 | 32 | LogView.super.update(self) 33 | end 34 | 35 | 36 | local function draw_text_multiline(font, text, x, y, color) 37 | local th = font:get_height() 38 | local resx, resy = x, y 39 | for line in text:gmatch("[^\n]+") do 40 | resy = y 41 | resx = renderer.draw_text(style.font, line, x, y, color) 42 | y = y + th 43 | end 44 | return resx, resy 45 | end 46 | 47 | 48 | function LogView:draw() 49 | self:draw_background(style.background) 50 | 51 | local ox, oy = self:get_content_offset() 52 | local th = style.font:get_height() 53 | local y = oy + style.padding.y + self.yoffset 54 | 55 | for i = #core.log_items, 1, -1 do 56 | local x = ox + style.padding.x 57 | local item = core.log_items[i] 58 | local time = os.date(nil, item.time) 59 | x = renderer.draw_text(style.font, time, x, y, style.dim) 60 | x = x + style.padding.x 61 | local subx = x 62 | x, y = draw_text_multiline(style.font, item.text, x, y, style.text) 63 | renderer.draw_text(style.font, " at " .. item.at, x, y, style.dim) 64 | y = y + th 65 | if item.info then 66 | subx, y = draw_text_multiline(style.font, item.info, subx, y, style.dim) 67 | y = y + th 68 | end 69 | y = y + style.padding.y 70 | end 71 | end 72 | 73 | 74 | return LogView 75 | -------------------------------------------------------------------------------- /data/plugins/language_python.lua: -------------------------------------------------------------------------------- 1 | local syntax = require "core.syntax" 2 | 3 | syntax.add { 4 | files = { "%.py$", "%.pyw$" }, 5 | headers = "^#!.*[ /]python", 6 | comment = "#", 7 | patterns = { 8 | { pattern = { "#", "\n" }, type = "comment" }, 9 | { pattern = { '[ruU]?"', '"', '\\' }, type = "string" }, 10 | { pattern = { "[ruU]?'", "'", '\\' }, type = "string" }, 11 | { pattern = { '"""', '"""' }, type = "string" }, 12 | { pattern = "0x[%da-fA-F]+", type = "number" }, 13 | { pattern = "-?%d+[%d%.eE]*", type = "number" }, 14 | { pattern = "-?%.?%d+", type = "number" }, 15 | { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" }, 16 | { pattern = "[%a_][%w_]*%f[(]", type = "function" }, 17 | { pattern = "[%a_][%w_]*", type = "symbol" }, 18 | }, 19 | symbols = { 20 | ["class"] = "keyword", 21 | ["finally"] = "keyword", 22 | ["is"] = "keyword", 23 | ["return"] = "keyword", 24 | ["continue"] = "keyword", 25 | ["for"] = "keyword", 26 | ["lambda"] = "keyword", 27 | ["try"] = "keyword", 28 | ["def"] = "keyword", 29 | ["from"] = "keyword", 30 | ["nonlocal"] = "keyword", 31 | ["while"] = "keyword", 32 | ["and"] = "keyword", 33 | ["global"] = "keyword", 34 | ["not"] = "keyword", 35 | ["with"] = "keyword", 36 | ["as"] = "keyword", 37 | ["elif"] = "keyword", 38 | ["if"] = "keyword", 39 | ["or"] = "keyword", 40 | ["else"] = "keyword", 41 | ["import"] = "keyword", 42 | ["pass"] = "keyword", 43 | ["break"] = "keyword", 44 | ["except"] = "keyword", 45 | ["in"] = "keyword", 46 | ["del"] = "keyword", 47 | ["raise"] = "keyword", 48 | ["yield"] = "keyword", 49 | ["assert"] = "keyword", 50 | ["self"] = "keyword2", 51 | ["None"] = "literal", 52 | ["True"] = "literal", 53 | ["False"] = "literal", 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /data/plugins/language_c.lua: -------------------------------------------------------------------------------- 1 | local syntax = require "core.syntax" 2 | 3 | syntax.add { 4 | files = { "%.c$", "%.h$", "%.inl$", "%.cpp$", "%.hpp$" }, 5 | comment = "//", 6 | patterns = { 7 | { pattern = "//.-\n", type = "comment" }, 8 | { pattern = { "/%*", "%*/" }, type = "comment" }, 9 | { pattern = { "#", "[^\\]\n" }, type = "comment" }, 10 | { pattern = { '"', '"', '\\' }, type = "string" }, 11 | { pattern = { "'", "'", '\\' }, type = "string" }, 12 | { pattern = "-?0x%x+", type = "number" }, 13 | { pattern = "-?%d+[%d%.eE]*f?", type = "number" }, 14 | { pattern = "-?%.?%d+f?", type = "number" }, 15 | { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" }, 16 | { pattern = "[%a_][%w_]*%f[(]", type = "function" }, 17 | { pattern = "[%a_][%w_]*", type = "symbol" }, 18 | }, 19 | symbols = { 20 | ["if"] = "keyword", 21 | ["then"] = "keyword", 22 | ["else"] = "keyword", 23 | ["elseif"] = "keyword", 24 | ["do"] = "keyword", 25 | ["while"] = "keyword", 26 | ["for"] = "keyword", 27 | ["break"] = "keyword", 28 | ["continue"] = "keyword", 29 | ["return"] = "keyword", 30 | ["goto"] = "keyword", 31 | ["struct"] = "keyword", 32 | ["union"] = "keyword", 33 | ["typedef"] = "keyword", 34 | ["enum"] = "keyword", 35 | ["extern"] = "keyword", 36 | ["static"] = "keyword", 37 | ["volatile"] = "keyword", 38 | ["const"] = "keyword", 39 | ["inline"] = "keyword", 40 | ["switch"] = "keyword", 41 | ["case"] = "keyword", 42 | ["default"] = "keyword", 43 | ["auto"] = "keyword", 44 | ["const"] = "keyword", 45 | ["void"] = "keyword", 46 | ["int"] = "keyword2", 47 | ["short"] = "keyword2", 48 | ["long"] = "keyword2", 49 | ["float"] = "keyword2", 50 | ["double"] = "keyword2", 51 | ["char"] = "keyword2", 52 | ["unsigned"] = "keyword2", 53 | ["bool"] = "keyword2", 54 | ["true"] = "literal", 55 | ["false"] = "literal", 56 | ["NULL"] = "literal", 57 | }, 58 | } 59 | 60 | -------------------------------------------------------------------------------- /api_renderer_font.odin: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "base:runtime" 4 | 5 | import lua "vendor:lua/5.4" 6 | 7 | f_load :: proc "c" (L: ^lua.State) -> i32 { 8 | context = runtime.default_context() 9 | filename: cstring = lua.L_checkstring(L, 1) 10 | size: f32 = cast(f32)lua.L_checknumber(L, 2) 11 | self: ^^RenFont = cast(^^RenFont)lua.newuserdata(L, size_of(^RenFont)) 12 | lua.L_setmetatable(L, API_TYPE_FONT) 13 | self^ = ren_load_font(filename, size) 14 | if (self^ == nil) { 15 | lua.L_error(L, "failed to load font") 16 | } 17 | return 1 18 | } 19 | 20 | f_set_tab_width :: proc "c" (L: ^lua.State) -> i32 { 21 | context = runtime.default_context() 22 | self: ^^RenFont = cast(^^RenFont)lua.L_checkudata(L, 1, API_TYPE_FONT) 23 | n := lua.L_checknumber(L, 2) 24 | ren_set_font_tab_width(self^, i32(n)) 25 | return 0 26 | } 27 | 28 | f_gc :: proc "c" (L: ^lua.State) -> i32 { 29 | self: ^^RenFont = cast(^^RenFont)lua.L_checkudata(L, 1, API_TYPE_FONT) 30 | context = runtime.default_context() 31 | if (self^ != nil) {rencache_free_font(self^)} 32 | return 0 33 | } 34 | 35 | 36 | f_get_width :: proc "c" (L: ^lua.State) -> i32 { 37 | self: ^^RenFont = cast(^^RenFont)lua.L_checkudata(L, 1, API_TYPE_FONT) 38 | text: cstring = lua.L_checkstring(L, 2) 39 | context = runtime.default_context() 40 | lua.pushnumber(L, cast(lua.Number)ren_get_font_width(self^, text)) 41 | return 1 42 | } 43 | 44 | 45 | f_get_height :: proc "c" (L: ^lua.State) -> i32 { 46 | self: ^^RenFont = cast(^^RenFont)lua.L_checkudata(L, 1, API_TYPE_FONT) 47 | lua.pushnumber(L, cast(lua.Number)ren_get_font_height(self^)) 48 | return 1 49 | } 50 | 51 | 52 | // odinfmt: disable 53 | @(private="file") 54 | lib := []lua.L_Reg { 55 | { "__gc", f_gc }, 56 | { "load", f_load }, 57 | { "set_tab_width", f_set_tab_width }, 58 | { "get_width", f_get_width }, 59 | { "get_height", f_get_height }, 60 | { nil, nil }, 61 | } 62 | // odinfmt: enable 63 | 64 | luaopen_renderer_font :: proc "c" (L: ^lua.State) -> i32 { 65 | lua.L_newmetatable(L, API_TYPE_FONT) 66 | lua.L_setfuncs(L, raw_data(lib), 0) 67 | lua.pushvalue(L, -1) 68 | lua.setfield(L, -2, "__index") 69 | return 1 70 | } 71 | 72 | -------------------------------------------------------------------------------- /data/core/doc/highlighter.lua: -------------------------------------------------------------------------------- 1 | local core = require "core" 2 | local config = require "core.config" 3 | local tokenizer = require "core.tokenizer" 4 | local Object = require "core.object" 5 | 6 | 7 | local Highlighter = Object:extend() 8 | 9 | 10 | function Highlighter:new(doc) 11 | self.doc = doc 12 | self:reset() 13 | 14 | -- init incremental syntax highlighting 15 | core.add_thread(function() 16 | while true do 17 | if self.first_invalid_line > self.max_wanted_line then 18 | self.max_wanted_line = 0 19 | coroutine.yield(1 / config.fps) 20 | 21 | else 22 | local max = math.min(self.first_invalid_line + 40, self.max_wanted_line) 23 | 24 | for i = self.first_invalid_line, max do 25 | local state = (i > 1) and self.lines[i - 1].state 26 | local line = self.lines[i] 27 | if not (line and line.init_state == state) then 28 | self.lines[i] = self:tokenize_line(i, state) 29 | end 30 | end 31 | 32 | self.first_invalid_line = max + 1 33 | core.redraw = true 34 | coroutine.yield() 35 | end 36 | end 37 | end, self) 38 | end 39 | 40 | 41 | function Highlighter:reset() 42 | self.lines = {} 43 | self.first_invalid_line = 1 44 | self.max_wanted_line = 0 45 | end 46 | 47 | 48 | function Highlighter:invalidate(idx) 49 | self.first_invalid_line = math.min(self.first_invalid_line, idx) 50 | self.max_wanted_line = math.min(self.max_wanted_line, #self.doc.lines) 51 | end 52 | 53 | 54 | function Highlighter:tokenize_line(idx, state) 55 | local res = {} 56 | res.init_state = state 57 | res.text = self.doc.lines[idx] 58 | res.tokens, res.state = tokenizer.tokenize(self.doc.syntax, res.text, state) 59 | return res 60 | end 61 | 62 | 63 | function Highlighter:get_line(idx) 64 | local line = self.lines[idx] 65 | if not line or line.text ~= self.doc.lines[idx] then 66 | local prev = self.lines[idx - 1] 67 | line = self:tokenize_line(idx, prev and prev.state) 68 | self.lines[idx] = line 69 | end 70 | self.max_wanted_line = math.max(self.max_wanted_line, idx) 71 | return line 72 | end 73 | 74 | 75 | function Highlighter:each_token(idx) 76 | return tokenizer.each_token(self:get_line(idx).tokens) 77 | end 78 | 79 | 80 | return Highlighter 81 | -------------------------------------------------------------------------------- /data/plugins/language_js.lua: -------------------------------------------------------------------------------- 1 | local syntax = require "core.syntax" 2 | 3 | syntax.add { 4 | files = { "%.js$", "%.json$", "%.cson$" }, 5 | comment = "//", 6 | patterns = { 7 | { pattern = "//.-\n", type = "comment" }, 8 | { pattern = { "/%*", "%*/" }, type = "comment" }, 9 | { pattern = { '"', '"', '\\' }, type = "string" }, 10 | { pattern = { "'", "'", '\\' }, type = "string" }, 11 | { pattern = { "`", "`", '\\' }, type = "string" }, 12 | { pattern = "0x[%da-fA-F]+", type = "number" }, 13 | { pattern = "-?%d+[%d%.eE]*", type = "number" }, 14 | { pattern = "-?%.?%d+", type = "number" }, 15 | { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" }, 16 | { pattern = "[%a_][%w_]*%f[(]", type = "function" }, 17 | { pattern = "[%a_][%w_]*", type = "symbol" }, 18 | }, 19 | symbols = { 20 | ["async"] = "keyword", 21 | ["await"] = "keyword", 22 | ["break"] = "keyword", 23 | ["case"] = "keyword", 24 | ["catch"] = "keyword", 25 | ["class"] = "keyword", 26 | ["const"] = "keyword", 27 | ["continue"] = "keyword", 28 | ["debugger"] = "keyword", 29 | ["default"] = "keyword", 30 | ["delete"] = "keyword", 31 | ["do"] = "keyword", 32 | ["else"] = "keyword", 33 | ["export"] = "keyword", 34 | ["extends"] = "keyword", 35 | ["finally"] = "keyword", 36 | ["for"] = "keyword", 37 | ["function"] = "keyword", 38 | ["get"] = "keyword", 39 | ["if"] = "keyword", 40 | ["import"] = "keyword", 41 | ["in"] = "keyword", 42 | ["instanceof"] = "keyword", 43 | ["let"] = "keyword", 44 | ["new"] = "keyword", 45 | ["return"] = "keyword", 46 | ["set"] = "keyword", 47 | ["static"] = "keyword", 48 | ["super"] = "keyword", 49 | ["switch"] = "keyword", 50 | ["throw"] = "keyword", 51 | ["try"] = "keyword", 52 | ["typeof"] = "keyword", 53 | ["var"] = "keyword", 54 | ["void"] = "keyword", 55 | ["while"] = "keyword", 56 | ["with"] = "keyword", 57 | ["yield"] = "keyword", 58 | ["true"] = "literal", 59 | ["false"] = "literal", 60 | ["null"] = "literal", 61 | ["undefined"] = "literal", 62 | ["arguments"] = "keyword2", 63 | ["Infinity"] = "keyword2", 64 | ["NaN"] = "keyword2", 65 | ["this"] = "keyword2", 66 | }, 67 | } 68 | -------------------------------------------------------------------------------- /data/core/tokenizer.lua: -------------------------------------------------------------------------------- 1 | local tokenizer = {} 2 | 3 | 4 | local function push_token(t, type, text) 5 | local prev_type = t[#t-1] 6 | local prev_text = t[#t] 7 | if prev_type and (prev_type == type or prev_text:find("^%s*$")) then 8 | t[#t-1] = type 9 | t[#t] = prev_text .. text 10 | else 11 | table.insert(t, type) 12 | table.insert(t, text) 13 | end 14 | end 15 | 16 | 17 | local function is_escaped(text, idx, esc) 18 | local byte = esc:byte() 19 | local count = 0 20 | for i = idx - 1, 1, -1 do 21 | if text:byte(i) ~= byte then break end 22 | count = count + 1 23 | end 24 | return count % 2 == 1 25 | end 26 | 27 | 28 | local function find_non_escaped(text, pattern, offset, esc) 29 | while true do 30 | local s, e = text:find(pattern, offset) 31 | if not s then break end 32 | if esc and is_escaped(text, s, esc) then 33 | offset = e + 1 34 | else 35 | return s, e 36 | end 37 | end 38 | end 39 | 40 | 41 | function tokenizer.tokenize(syntax, text, state) 42 | local res = {} 43 | local i = 1 44 | 45 | if #syntax.patterns == 0 then 46 | return { "normal", text } 47 | end 48 | 49 | while i <= #text do 50 | -- continue trying to match the end pattern of a pair if we have a state set 51 | if state then 52 | local p = syntax.patterns[state] 53 | local s, e = find_non_escaped(text, p.pattern[2], i, p.pattern[3]) 54 | 55 | if s then 56 | push_token(res, p.type, text:sub(i, e)) 57 | state = nil 58 | i = e + 1 59 | else 60 | push_token(res, p.type, text:sub(i)) 61 | break 62 | end 63 | end 64 | 65 | -- find matching pattern 66 | local matched = false 67 | for n, p in ipairs(syntax.patterns) do 68 | local pattern = (type(p.pattern) == "table") and p.pattern[1] or p.pattern 69 | local s, e = text:find("^" .. pattern, i) 70 | 71 | if s then 72 | -- matched pattern; make and add token 73 | local t = text:sub(s, e) 74 | push_token(res, syntax.symbols[t] or p.type, t) 75 | 76 | -- update state if this was a start|end pattern pair 77 | if type(p.pattern) == "table" then 78 | state = n 79 | end 80 | 81 | -- move cursor past this token 82 | i = e + 1 83 | matched = true 84 | break 85 | end 86 | end 87 | 88 | -- consume character if we didn't match 89 | if not matched then 90 | push_token(res, "normal", text:sub(i, i)) 91 | i = i + 1 92 | end 93 | end 94 | 95 | return res, state 96 | end 97 | 98 | 99 | local function iter(t, i) 100 | i = i + 2 101 | local type, text = t[i], t[i+1] 102 | if type then 103 | return i, type, text 104 | end 105 | end 106 | 107 | function tokenizer.each_token(t) 108 | return iter, t, -1 109 | end 110 | 111 | 112 | return tokenizer 113 | -------------------------------------------------------------------------------- /data/core/commands/core.lua: -------------------------------------------------------------------------------- 1 | local core = require "core" 2 | local common = require "core.common" 3 | local command = require "core.command" 4 | local keymap = require "core.keymap" 5 | local LogView = require "core.logview" 6 | 7 | 8 | local fullscreen = false 9 | 10 | command.add(nil, { 11 | ["core:quit"] = function() 12 | core.quit() 13 | end, 14 | 15 | ["core:force-quit"] = function() 16 | core.quit(true) 17 | end, 18 | 19 | ["core:toggle-fullscreen"] = function() 20 | fullscreen = not fullscreen 21 | system.set_window_mode(fullscreen and "fullscreen" or "normal") 22 | end, 23 | 24 | ["core:reload-module"] = function() 25 | core.command_view:enter("Reload Module", function(text, item) 26 | local text = item and item.text or text 27 | core.reload_module(text) 28 | core.log("Reloaded module %q", text) 29 | end, function(text) 30 | local items = {} 31 | for name in pairs(package.loaded) do 32 | table.insert(items, name) 33 | end 34 | return common.fuzzy_match(items, text) 35 | end) 36 | end, 37 | 38 | ["core:find-command"] = function() 39 | local commands = command.get_all_valid() 40 | core.command_view:enter("Do Command", function(text, item) 41 | if item then 42 | command.perform(item.command) 43 | end 44 | end, function(text) 45 | local res = common.fuzzy_match(commands, text) 46 | for i, name in ipairs(res) do 47 | res[i] = { 48 | text = command.prettify_name(name), 49 | info = keymap.get_binding(name), 50 | command = name, 51 | } 52 | end 53 | return res 54 | end) 55 | end, 56 | 57 | ["core:find-file"] = function() 58 | core.command_view:enter("Open File From Project", function(text, item) 59 | text = item and item.text or text 60 | core.root_view:open_doc(core.open_doc(text)) 61 | end, function(text) 62 | local files = {} 63 | for _, item in pairs(core.project_files) do 64 | if item.type == "file" then 65 | table.insert(files, item.filename) 66 | end 67 | end 68 | return common.fuzzy_match(files, text) 69 | end) 70 | end, 71 | 72 | ["core:new-doc"] = function() 73 | core.root_view:open_doc(core.open_doc()) 74 | end, 75 | 76 | ["core:open-file"] = function() 77 | core.command_view:enter("Open File", function(text) 78 | core.root_view:open_doc(core.open_doc(text)) 79 | end, common.path_suggest) 80 | end, 81 | 82 | ["core:open-log"] = function() 83 | local node = core.root_view:get_active_node() 84 | node:add_view(LogView()) 85 | end, 86 | 87 | ["core:open-user-module"] = function() 88 | core.root_view:open_doc(core.open_doc(EXEDIR .. "/data/user/init.lua")) 89 | end, 90 | 91 | ["core:open-project-module"] = function() 92 | local filename = ".lite_project.lua" 93 | if system.get_file_info(filename) then 94 | core.root_view:open_doc(core.open_doc(filename)) 95 | else 96 | local doc = core.open_doc() 97 | core.root_view:open_doc(doc) 98 | doc:save(filename) 99 | end 100 | end, 101 | }) 102 | -------------------------------------------------------------------------------- /api_renderer.odin: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "base:runtime" 4 | 5 | import lua "vendor:lua/5.4" 6 | 7 | @(private = "file") 8 | checkcolor :: proc(L: ^lua.State, idx: i32, default: i32) -> RenColor { 9 | if (lua.isnoneornil(L, idx)) { 10 | return RenColor{u8(default), u8(default), u8(default), 255} 11 | } 12 | lua.rawgeti(L, idx, 1) 13 | lua.rawgeti(L, idx, 2) 14 | lua.rawgeti(L, idx, 3) 15 | lua.rawgeti(L, idx, 4) 16 | color: RenColor 17 | color.r = cast(u8)lua.L_checknumber(L, -4) 18 | color.g = cast(u8)lua.L_checknumber(L, -3) 19 | color.b = cast(u8)lua.L_checknumber(L, -2) 20 | color.a = cast(u8)lua.L_optnumber(L, -1, 255) 21 | lua.pop(L, 4) 22 | return color 23 | } 24 | 25 | f_show_debug :: proc "c" (L: ^lua.State) -> i32 { 26 | context = runtime.default_context() 27 | lua.L_checkany(L, 1) 28 | rencache_show_debug(cast(bool)lua.toboolean(L, 1)) 29 | return 0 30 | } 31 | 32 | f_get_size :: proc "c" (L: ^lua.State) -> i32 { 33 | w, h: i32 34 | ren_get_size(&w, &h) 35 | lua.pushnumber(L, lua.Number(w)) 36 | lua.pushnumber(L, lua.Number(h)) 37 | return 2 38 | } 39 | 40 | f_begin_frame :: proc "c" (L: ^lua.State) -> i32 { 41 | context = runtime.default_context() 42 | rencache_begin_frame() 43 | return 0 44 | } 45 | 46 | f_end_frame :: proc "c" (L: ^lua.State) -> i32 { 47 | context = runtime.default_context() 48 | rencache_end_frame() 49 | return 0 50 | } 51 | 52 | f_set_clip_rect :: proc "c" (L: ^lua.State) -> i32 { 53 | rect: RenRect 54 | rect.x = cast(i32)lua.L_checknumber(L, 1) 55 | rect.y = cast(i32)lua.L_checknumber(L, 2) 56 | rect.width = cast(i32)lua.L_checknumber(L, 3) 57 | rect.height = cast(i32)lua.L_checknumber(L, 4) 58 | context = runtime.default_context() 59 | rencache_set_clip_rect(rect) 60 | return 0 61 | } 62 | 63 | f_draw_rect :: proc "c" (L: ^lua.State) -> i32 { 64 | context = runtime.default_context() 65 | rect: RenRect 66 | rect.x = cast(i32)lua.L_checknumber(L, 1) 67 | rect.y = cast(i32)lua.L_checknumber(L, 2) 68 | rect.width = cast(i32)lua.L_checknumber(L, 3) 69 | rect.height = cast(i32)lua.L_checknumber(L, 4) 70 | color: RenColor = checkcolor(L, 5, 255) 71 | rencache_draw_rect(rect, color) 72 | return 0 73 | } 74 | 75 | f_draw_text :: proc "c" (L: ^lua.State) -> i32 { 76 | context = runtime.default_context() 77 | font: ^^RenFont = cast(^^RenFont)lua.L_checkudata(L, 1, API_TYPE_FONT) 78 | text: cstring = lua.L_checkstring(L, 2) 79 | x: int = cast(int)lua.L_checknumber(L, 3) 80 | y: int = cast(int)lua.L_checknumber(L, 4) 81 | color: RenColor = checkcolor(L, 5, 255) 82 | x = rencache_draw_text(font^, text, x, y, color) 83 | lua.pushnumber(L, lua.Number(x)) 84 | return 1 85 | } 86 | 87 | // odinfmt: disable 88 | @(private="file") 89 | lib := []lua.L_Reg { 90 | { "show_debug", f_show_debug }, 91 | { "get_size", f_get_size }, 92 | { "begin_frame", f_begin_frame }, 93 | { "end_frame", f_end_frame }, 94 | { "set_clip_rect", f_set_clip_rect }, 95 | { "draw_rect", f_draw_rect }, 96 | { "draw_text", f_draw_text }, 97 | { nil, nil }, 98 | } 99 | // odinfmt: enable 100 | 101 | luaopen_renderer :: proc "c" (L: ^lua.State) -> i32 { 102 | context = runtime.default_context() 103 | lua.L_newlib(L, lib) 104 | luaopen_renderer_font(L) 105 | lua.setfield(L, -2, "font") 106 | return 1 107 | } 108 | 109 | -------------------------------------------------------------------------------- /data/core/commands/root.lua: -------------------------------------------------------------------------------- 1 | local core = require "core" 2 | local style = require "core.style" 3 | local DocView = require "core.docview" 4 | local command = require "core.command" 5 | local common = require "core.common" 6 | 7 | 8 | local t = { 9 | ["root:close"] = function() 10 | local node = core.root_view:get_active_node() 11 | node:close_active_view(core.root_view.root_node) 12 | end, 13 | 14 | ["root:switch-to-previous-tab"] = function() 15 | local node = core.root_view:get_active_node() 16 | local idx = node:get_view_idx(core.active_view) 17 | idx = idx - 1 18 | if idx < 1 then idx = #node.views end 19 | node:set_active_view(node.views[idx]) 20 | end, 21 | 22 | ["root:switch-to-next-tab"] = function() 23 | local node = core.root_view:get_active_node() 24 | local idx = node:get_view_idx(core.active_view) 25 | idx = idx + 1 26 | if idx > #node.views then idx = 1 end 27 | node:set_active_view(node.views[idx]) 28 | end, 29 | 30 | ["root:move-tab-left"] = function() 31 | local node = core.root_view:get_active_node() 32 | local idx = node:get_view_idx(core.active_view) 33 | if idx > 1 then 34 | table.remove(node.views, idx) 35 | table.insert(node.views, idx - 1, core.active_view) 36 | end 37 | end, 38 | 39 | ["root:move-tab-right"] = function() 40 | local node = core.root_view:get_active_node() 41 | local idx = node:get_view_idx(core.active_view) 42 | if idx < #node.views then 43 | table.remove(node.views, idx) 44 | table.insert(node.views, idx + 1, core.active_view) 45 | end 46 | end, 47 | 48 | ["root:shrink"] = function() 49 | local node = core.root_view:get_active_node() 50 | local parent = node:get_parent_node(core.root_view.root_node) 51 | local n = (parent.a == node) and -0.1 or 0.1 52 | parent.divider = common.clamp(parent.divider + n, 0.1, 0.9) 53 | end, 54 | 55 | ["root:grow"] = function() 56 | local node = core.root_view:get_active_node() 57 | local parent = node:get_parent_node(core.root_view.root_node) 58 | local n = (parent.a == node) and 0.1 or -0.1 59 | parent.divider = common.clamp(parent.divider + n, 0.1, 0.9) 60 | end, 61 | } 62 | 63 | 64 | for i = 1, 9 do 65 | t["root:switch-to-tab-" .. i] = function() 66 | local node = core.root_view:get_active_node() 67 | local view = node.views[i] 68 | if view then 69 | node:set_active_view(view) 70 | end 71 | end 72 | end 73 | 74 | 75 | for _, dir in ipairs { "left", "right", "up", "down" } do 76 | t["root:split-" .. dir] = function() 77 | local node = core.root_view:get_active_node() 78 | local av = node.active_view 79 | node:split(dir) 80 | if av:is(DocView) then 81 | core.root_view:open_doc(av.doc) 82 | end 83 | end 84 | 85 | t["root:switch-to-" .. dir] = function() 86 | local node = core.root_view:get_active_node() 87 | local x, y 88 | if dir == "left" or dir == "right" then 89 | y = node.position.y + node.size.y / 2 90 | x = node.position.x + (dir == "left" and -1 or node.size.x + style.divider_size) 91 | else 92 | x = node.position.x + node.size.x / 2 93 | y = node.position.y + (dir == "up" and -1 or node.size.y + style.divider_size) 94 | end 95 | local node = core.root_view.root_node:get_child_overlapping_point(x, y) 96 | if not node:get_locked_size() then 97 | core.set_active_view(node.active_view) 98 | end 99 | end 100 | end 101 | 102 | command.add(function() 103 | local node = core.root_view:get_active_node() 104 | return not node:get_locked_size() 105 | end, t) 106 | -------------------------------------------------------------------------------- /data/core/doc/translate.lua: -------------------------------------------------------------------------------- 1 | local common = require "core.common" 2 | local config = require "core.config" 3 | 4 | -- functions for translating a Doc position to another position these functions 5 | -- can be passed to Doc:move_to|select_to|delete_to() 6 | 7 | local translate = {} 8 | 9 | 10 | local function is_non_word(char) 11 | return config.non_word_chars:find(char, nil, true) 12 | end 13 | 14 | 15 | function translate.previous_char(doc, line, col) 16 | repeat 17 | line, col = doc:position_offset(line, col, -1) 18 | until not common.is_utf8_cont(doc:get_char(line, col)) 19 | return line, col 20 | end 21 | 22 | 23 | function translate.next_char(doc, line, col) 24 | repeat 25 | line, col = doc:position_offset(line, col, 1) 26 | until not common.is_utf8_cont(doc:get_char(line, col)) 27 | return line, col 28 | end 29 | 30 | 31 | function translate.previous_word_start(doc, line, col) 32 | local prev 33 | while line > 1 or col > 1 do 34 | local l, c = doc:position_offset(line, col, -1) 35 | local char = doc:get_char(l, c) 36 | if prev and prev ~= char or not is_non_word(char) then 37 | break 38 | end 39 | prev, line, col = char, l, c 40 | end 41 | return translate.start_of_word(doc, line, col) 42 | end 43 | 44 | 45 | function translate.next_word_end(doc, line, col) 46 | local prev 47 | local end_line, end_col = translate.end_of_doc(doc, line, col) 48 | while line < end_line or col < end_col do 49 | local char = doc:get_char(line, col) 50 | if prev and prev ~= char or not is_non_word(char) then 51 | break 52 | end 53 | line, col = doc:position_offset(line, col, 1) 54 | prev = char 55 | end 56 | return translate.end_of_word(doc, line, col) 57 | end 58 | 59 | 60 | function translate.start_of_word(doc, line, col) 61 | while true do 62 | local line2, col2 = doc:position_offset(line, col, -1) 63 | local char = doc:get_char(line2, col2) 64 | if is_non_word(char) 65 | or line == line2 and col == col2 then 66 | break 67 | end 68 | line, col = line2, col2 69 | end 70 | return line, col 71 | end 72 | 73 | 74 | function translate.end_of_word(doc, line, col) 75 | while true do 76 | local line2, col2 = doc:position_offset(line, col, 1) 77 | local char = doc:get_char(line, col) 78 | if is_non_word(char) 79 | or line == line2 and col == col2 then 80 | break 81 | end 82 | line, col = line2, col2 83 | end 84 | return line, col 85 | end 86 | 87 | 88 | function translate.previous_block_start(doc, line, col) 89 | while true do 90 | line = line - 1 91 | if line <= 1 then 92 | return 1, 1 93 | end 94 | if doc.lines[line-1]:find("^%s*$") 95 | and not doc.lines[line]:find("^%s*$") then 96 | return line, (doc.lines[line]:find("%S")) 97 | end 98 | end 99 | end 100 | 101 | 102 | function translate.next_block_end(doc, line, col) 103 | while true do 104 | if line >= #doc.lines then 105 | return #doc.lines, 1 106 | end 107 | if doc.lines[line+1]:find("^%s*$") 108 | and not doc.lines[line]:find("^%s*$") then 109 | return line+1, #doc.lines[line+1] 110 | end 111 | line = line + 1 112 | end 113 | end 114 | 115 | 116 | function translate.start_of_line(doc, line, col) 117 | return line, 1 118 | end 119 | 120 | 121 | function translate.end_of_line(doc, line, col) 122 | return line, math.huge 123 | end 124 | 125 | 126 | function translate.start_of_doc(doc, line, col) 127 | return 1, 1 128 | end 129 | 130 | 131 | function translate.end_of_doc(doc, line, col) 132 | return #doc.lines, #doc.lines[#doc.lines] 133 | end 134 | 135 | 136 | return translate 137 | -------------------------------------------------------------------------------- /data/core/common.lua: -------------------------------------------------------------------------------- 1 | local common = {} 2 | 3 | 4 | function common.is_utf8_cont(char) 5 | local byte = char:byte() 6 | return byte >= 0x80 and byte < 0xc0 7 | end 8 | 9 | 10 | function common.utf8_chars(text) 11 | return text:gmatch("[\0-\x7f\xc2-\xf4][\x80-\xbf]*") 12 | end 13 | 14 | 15 | function common.clamp(n, lo, hi) 16 | return math.max(math.min(n, hi), lo) 17 | end 18 | 19 | 20 | function common.round(n) 21 | return n >= 0 and math.floor(n + 0.5) or math.ceil(n - 0.5) 22 | end 23 | 24 | 25 | function common.lerp(a, b, t) 26 | if type(a) ~= "table" then 27 | return a + (b - a) * t 28 | end 29 | local res = {} 30 | for k, v in pairs(b) do 31 | res[k] = common.lerp(a[k], v, t) 32 | end 33 | return res 34 | end 35 | 36 | 37 | function common.color(str) 38 | local r, g, b, a = str:match("#(%x%x)(%x%x)(%x%x)") 39 | if r then 40 | r = tonumber(r, 16) 41 | g = tonumber(g, 16) 42 | b = tonumber(b, 16) 43 | a = 1 44 | elseif str:match("rgba?%s*%([%d%s%.,]+%)") then 45 | local f = str:gmatch("[%d.]+") 46 | r = (f() or 0) 47 | g = (f() or 0) 48 | b = (f() or 0) 49 | a = f() or 1 50 | else 51 | error(string.format("bad color string '%s'", str)) 52 | end 53 | return r, g, b, a * 0xff 54 | end 55 | 56 | 57 | local function compare_score(a, b) 58 | return a.score > b.score 59 | end 60 | 61 | local function fuzzy_match_items(items, needle) 62 | local res = {} 63 | for _, item in ipairs(items) do 64 | local score = system.fuzzy_match(tostring(item), needle) 65 | if score then 66 | table.insert(res, { text = item, score = score }) 67 | end 68 | end 69 | table.sort(res, compare_score) 70 | for i, item in ipairs(res) do 71 | res[i] = item.text 72 | end 73 | return res 74 | end 75 | 76 | 77 | function common.fuzzy_match(haystack, needle) 78 | if type(haystack) == "table" then 79 | return fuzzy_match_items(haystack, needle) 80 | end 81 | return system.fuzzy_match(haystack, needle) 82 | end 83 | 84 | 85 | function common.path_suggest(text) 86 | local path, name = text:match("^(.-)([^/\\]*)$") 87 | local files = system.list_dir(path == "" and "." or path) or {} 88 | local res = {} 89 | for _, file in ipairs(files) do 90 | file = path .. file 91 | local info = system.get_file_info(file) 92 | if info then 93 | if info.type == "dir" then 94 | file = file .. PATHSEP 95 | end 96 | if file:lower():find(text:lower(), nil, true) == 1 then 97 | table.insert(res, file) 98 | end 99 | end 100 | end 101 | return res 102 | end 103 | 104 | 105 | function common.match_pattern(text, pattern, ...) 106 | if type(pattern) == "string" then 107 | return text:find(pattern, ...) 108 | end 109 | for _, p in ipairs(pattern) do 110 | local s, e = common.match_pattern(text, p, ...) 111 | if s then return s, e end 112 | end 113 | return false 114 | end 115 | 116 | 117 | function common.draw_text(font, color, text, align, x,y,w,h) 118 | local tw, th = font:get_width(text), font:get_height(text) 119 | if align == "center" then 120 | x = x + (w - tw) / 2 121 | elseif align == "right" then 122 | x = x + (w - tw) 123 | end 124 | y = common.round(y + (h - th) / 2) 125 | return renderer.draw_text(font, text, x, y, color), y + th 126 | end 127 | 128 | 129 | function common.bench(name, fn, ...) 130 | local start = system.get_time() 131 | local res = fn(...) 132 | local t = system.get_time() - start 133 | local ms = t * 1000 134 | local per = (t / (1 / 60)) * 100 135 | print(string.format("*** %-16s : %8.3fms %6.2f%%", name, ms, per)) 136 | return res 137 | end 138 | 139 | 140 | return common 141 | -------------------------------------------------------------------------------- /data/core/view.lua: -------------------------------------------------------------------------------- 1 | local core = require "core" 2 | local config = require "core.config" 3 | local style = require "core.style" 4 | local common = require "core.common" 5 | local Object = require "core.object" 6 | 7 | 8 | local View = Object:extend() 9 | 10 | 11 | function View:new() 12 | self.position = { x = 0, y = 0 } 13 | self.size = { x = 0, y = 0 } 14 | self.scroll = { x = 0, y = 0, to = { x = 0, y = 0 } } 15 | self.cursor = "arrow" 16 | self.scrollable = false 17 | end 18 | 19 | 20 | function View:move_towards(t, k, dest, rate) 21 | if type(t) ~= "table" then 22 | return self:move_towards(self, t, k, dest, rate) 23 | end 24 | local val = t[k] 25 | if math.abs(val - dest) < 0.5 then 26 | t[k] = dest 27 | else 28 | t[k] = common.lerp(val, dest, rate or 0.5) 29 | end 30 | if val ~= dest then 31 | core.redraw = true 32 | end 33 | end 34 | 35 | 36 | function View:try_close(do_close) 37 | do_close() 38 | end 39 | 40 | 41 | function View:get_name() 42 | return "---" 43 | end 44 | 45 | 46 | function View:get_scrollable_size() 47 | return math.huge 48 | end 49 | 50 | 51 | function View:get_scrollbar_rect() 52 | local sz = self:get_scrollable_size() 53 | if sz <= self.size.y or sz == math.huge then 54 | return 0, 0, 0, 0 55 | end 56 | local h = math.max(20, self.size.y * self.size.y / sz) 57 | return 58 | self.position.x + self.size.x - style.scrollbar_size, 59 | self.position.y + self.scroll.y * (self.size.y - h) / (sz - self.size.y), 60 | style.scrollbar_size, 61 | h 62 | end 63 | 64 | 65 | function View:scrollbar_overlaps_point(x, y) 66 | local sx, sy, sw, sh = self:get_scrollbar_rect() 67 | return x >= sx - sw * 3 and x < sx + sw and y >= sy and y < sy + sh 68 | end 69 | 70 | 71 | function View:on_mouse_pressed(button, x, y, clicks) 72 | if self:scrollbar_overlaps_point(x, y) then 73 | self.dragging_scrollbar = true 74 | return true 75 | end 76 | end 77 | 78 | 79 | function View:on_mouse_released(button, x, y) 80 | self.dragging_scrollbar = false 81 | end 82 | 83 | 84 | function View:on_mouse_moved(x, y, dx, dy) 85 | if self.dragging_scrollbar then 86 | local delta = self:get_scrollable_size() / self.size.y * dy 87 | self.scroll.to.y = self.scroll.to.y + delta 88 | end 89 | self.hovered_scrollbar = self:scrollbar_overlaps_point(x, y) 90 | end 91 | 92 | 93 | function View:on_text_input(text) 94 | -- no-op 95 | end 96 | 97 | 98 | function View:on_mouse_wheel(y) 99 | if self.scrollable then 100 | self.scroll.to.y = self.scroll.to.y + y * -config.mouse_wheel_scroll 101 | end 102 | end 103 | 104 | 105 | function View:get_content_bounds() 106 | local x = self.scroll.x 107 | local y = self.scroll.y 108 | return x, y, x + self.size.x, y + self.size.y 109 | end 110 | 111 | 112 | function View:get_content_offset() 113 | local x = common.round(self.position.x - self.scroll.x) 114 | local y = common.round(self.position.y - self.scroll.y) 115 | return x, y 116 | end 117 | 118 | 119 | function View:clamp_scroll_position() 120 | local max = self:get_scrollable_size() - self.size.y 121 | self.scroll.to.y = common.clamp(self.scroll.to.y, 0, max) 122 | end 123 | 124 | 125 | function View:update() 126 | self:clamp_scroll_position() 127 | self:move_towards(self.scroll, "x", self.scroll.to.x, 0.3) 128 | self:move_towards(self.scroll, "y", self.scroll.to.y, 0.3) 129 | end 130 | 131 | 132 | function View:draw_background(color) 133 | local x, y = self.position.x, self.position.y 134 | local w, h = self.size.x, self.size.y 135 | renderer.draw_rect(x, y, w + x % 1, h + y % 1, color) 136 | end 137 | 138 | 139 | function View:draw_scrollbar() 140 | local x, y, w, h = self:get_scrollbar_rect() 141 | local highlight = self.hovered_scrollbar or self.dragging_scrollbar 142 | local color = highlight and style.scrollbar2 or style.scrollbar 143 | renderer.draw_rect(x, y, w, h, color) 144 | end 145 | 146 | 147 | function View:draw() 148 | end 149 | 150 | 151 | return View 152 | -------------------------------------------------------------------------------- /data/core/statusview.lua: -------------------------------------------------------------------------------- 1 | local core = require "core" 2 | local common = require "core.common" 3 | local command = require "core.command" 4 | local config = require "core.config" 5 | local style = require "core.style" 6 | local DocView = require "core.docview" 7 | local LogView = require "core.logview" 8 | local View = require "core.view" 9 | 10 | 11 | local StatusView = View:extend() 12 | 13 | StatusView.separator = " " 14 | StatusView.separator2 = " | " 15 | 16 | 17 | function StatusView:new() 18 | StatusView.super.new(self) 19 | self.message_timeout = 0 20 | self.message = {} 21 | end 22 | 23 | 24 | function StatusView:on_mouse_pressed() 25 | core.set_active_view(core.last_active_view) 26 | if system.get_time() < self.message_timeout 27 | and not core.active_view:is(LogView) then 28 | command.perform "core:open-log" 29 | end 30 | end 31 | 32 | 33 | function StatusView:show_message(icon, icon_color, text) 34 | self.message = { 35 | icon_color, style.icon_font, icon, 36 | style.dim, style.font, StatusView.separator2, style.text, text 37 | } 38 | self.message_timeout = system.get_time() + config.message_timeout 39 | end 40 | 41 | 42 | function StatusView:update() 43 | self.size.y = style.font:get_height() + style.padding.y * 2 44 | 45 | if system.get_time() < self.message_timeout then 46 | self.scroll.to.y = self.size.y 47 | else 48 | self.scroll.to.y = 0 49 | end 50 | 51 | StatusView.super.update(self) 52 | end 53 | 54 | 55 | local function draw_items(self, items, x, y, draw_fn) 56 | local font = style.font 57 | local color = style.text 58 | 59 | for _, item in ipairs(items) do 60 | if type(item) == "userdata" then 61 | font = item 62 | elseif type(item) == "table" then 63 | color = item 64 | else 65 | x = draw_fn(font, color, item, nil, x, y, 0, self.size.y) 66 | end 67 | end 68 | 69 | return x 70 | end 71 | 72 | 73 | local function text_width(font, _, text, _, x) 74 | return x + font:get_width(text) 75 | end 76 | 77 | 78 | function StatusView:draw_items(items, right_align, yoffset) 79 | local x, y = self:get_content_offset() 80 | y = y + (yoffset or 0) 81 | if right_align then 82 | local w = draw_items(self, items, 0, 0, text_width) 83 | x = x + self.size.x - w - style.padding.x 84 | draw_items(self, items, x, y, common.draw_text) 85 | else 86 | x = x + style.padding.x 87 | draw_items(self, items, x, y, common.draw_text) 88 | end 89 | end 90 | 91 | 92 | function StatusView:get_items() 93 | if getmetatable(core.active_view) == DocView then 94 | local dv = core.active_view 95 | local line, col = dv.doc:get_selection() 96 | local dirty = dv.doc:is_dirty() 97 | 98 | return { 99 | dirty and style.accent or style.text, style.icon_font, "f", 100 | style.dim, style.font, self.separator2, style.text, 101 | dv.doc.filename and style.text or style.dim, dv.doc:get_name(), 102 | style.text, 103 | self.separator, 104 | "line: ", line, 105 | self.separator, 106 | col > config.line_limit and style.accent or style.text, "col: ", col, 107 | style.text, 108 | self.separator, 109 | string.format("%d%%", tonumber(math.floor(line / #dv.doc.lines * 100))), 110 | }, { 111 | style.icon_font, "g", 112 | style.font, style.dim, self.separator2, style.text, 113 | #dv.doc.lines, " lines", 114 | self.separator, 115 | dv.doc.crlf and "CRLF" or "LF" 116 | } 117 | end 118 | 119 | return {}, { 120 | style.icon_font, "g", 121 | style.font, style.dim, self.separator2, 122 | #core.docs, style.text, " / ", 123 | #core.project_files, " files" 124 | } 125 | end 126 | 127 | 128 | function StatusView:draw() 129 | self:draw_background(style.background2) 130 | 131 | if self.message then 132 | self:draw_items(self.message, false, self.size.y) 133 | end 134 | 135 | local left, right = self:get_items() 136 | self:draw_items(left) 137 | self:draw_items(right, true) 138 | end 139 | 140 | 141 | return StatusView 142 | -------------------------------------------------------------------------------- /main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | 4 | const c = @cImport({ 5 | @cInclude("SDL2/SDL.h"); 6 | 7 | @cInclude("lua/lua.h"); 8 | @cInclude("lua/lauxlib.h"); 9 | @cInclude("lua/lualib.h"); 10 | 11 | @cInclude("C/renderer.h"); 12 | // @cInclude("C/api/api.h"); 13 | }); 14 | 15 | export var window: *c.SDL_Window = undefined; 16 | 17 | extern "c" fn api_load_libs(L: *c.lua_State) void; 18 | 19 | fn get_scale() f64 { 20 | var dpi: f32 = undefined; 21 | _ = c.SDL_GetDisplayDPI(0, null, &dpi, null); 22 | if (comptime builtin.os.tag == .windows) { 23 | return dpi / 96.0; 24 | } 25 | return 1.0; 26 | } 27 | 28 | fn init_window_icon() void { 29 | // TODO 30 | } 31 | 32 | pub fn main() !void { 33 | if (c.SDL_Init(c.SDL_INIT_VIDEO) != 0) { 34 | c.SDL_Log("Unable to initialize SDL: %s\n", c.SDL_GetError()); 35 | return error.SDLInitializationFailed; 36 | } 37 | defer c.SDL_Quit(); 38 | 39 | c.SDL_EnableScreenSaver(); 40 | // ret value can be ignored as it just returns the previous state 41 | _ = c.SDL_EventState(c.SDL_DROPFILE, c.SDL_ENABLE); 42 | 43 | _ = c.SDL_SetHint(c.SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1"); 44 | _ = c.SDL_SetHint(c.SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0"); 45 | 46 | var dm: c.SDL_DisplayMode = undefined; 47 | if (c.SDL_GetCurrentDisplayMode(0, &dm) != 0) { 48 | c.SDL_Log("Unable to get SDL_GetCurrentDisplayMode: %s\n", c.SDL_GetError()); 49 | } 50 | 51 | const width = @as(c_int, @intFromFloat(@as(f32, @floatFromInt(dm.w)) * 0.8)); 52 | const height = @as(c_int, @intFromFloat(@as(f32, @floatFromInt(dm.w)) * 0.8)); 53 | 54 | window = c.SDL_CreateWindow("", c.SDL_WINDOWPOS_UNDEFINED, c.SDL_WINDOWPOS_UNDEFINED, width, height, c.SDL_WINDOW_RESIZABLE | c.SDL_WINDOW_ALLOW_HIGHDPI | c.SDL_WINDOW_HIDDEN) orelse { 55 | std.log.err("Failed to create sdl window {s}", .{c.SDL_GetError()}); 56 | return error.SDLInitializationFailed; 57 | }; 58 | 59 | // init_window_icon(); 60 | c.ren_init(window); 61 | 62 | const L = c.luaL_newstate() orelse { 63 | std.log.err("Failed to create lua state", .{}); 64 | return error.Failed; 65 | }; 66 | defer c.lua_close(L); 67 | c.luaL_openlibs(L); 68 | 69 | api_load_libs(L); 70 | 71 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 72 | const allocator = gpa.allocator(); 73 | var args = try std.process.argsWithAllocator(allocator); 74 | defer args.deinit(); 75 | 76 | c.lua_newtable(L); 77 | var i: i32 = 0; 78 | while (args.next()) |arg| { 79 | _ = c.lua_pushstring(L, arg); 80 | c.lua_rawseti(L, -2, i + 1); 81 | i = i + 1; 82 | } 83 | c.lua_setglobal(L, "ARGS"); 84 | 85 | _ = c.lua_pushstring(L, "1.11"); 86 | c.lua_setglobal(L, "VERSION"); 87 | 88 | _ = c.lua_pushstring(L, c.SDL_GetPlatform()); 89 | c.lua_setglobal(L, "PLATFORM"); 90 | 91 | c.lua_pushnumber(L, get_scale()); 92 | c.lua_setglobal(L, "SCALE"); 93 | 94 | var exename: [512]u8 = undefined; 95 | @memset(&exename, 0); 96 | _ = std.fs.selfExePath(&exename) catch |err| { 97 | std.log.info("Failed to get exepath error: {}", .{err}); 98 | return err; 99 | }; 100 | _ = c.lua_pushstring(L, &exename); 101 | c.lua_setglobal(L, "EXEFILE"); 102 | 103 | std.log.info("Starting up...", .{}); 104 | 105 | const code = 106 | \\local core 107 | \\xpcall(function() 108 | \\ SCALE = tonumber(os.getenv("LITE_SCALE")) or SCALE 109 | \\ PATHSEP = package.config:sub(1, 1) 110 | \\ EXEDIR = EXEFILE:match("^(.+)[/\\].*$") 111 | \\ package.path = EXEDIR .. '/data/?.lua;' .. package.path 112 | \\ package.path = EXEDIR .. '/data/?/init.lua;' .. package.path 113 | \\ core = require('core') 114 | \\ core.init() 115 | \\ core.run() 116 | \\end, function(err) 117 | \\ print('Error: ' .. tostring(err)) 118 | \\ print(debug.traceback(nil, 2)) 119 | \\ if core and core.on_error then 120 | \\ pcall(core.on_error, err) 121 | \\ end 122 | \\ os.exit(1) 123 | \\end) 124 | ; 125 | var res = c.luaL_loadstring(L, code); 126 | if (res != c.LUA_OK) { 127 | std.log.err("Lua luaL_loadstring failure {}", .{res}); 128 | return error.LuaInitFailure; 129 | } 130 | 131 | res = c.lua_pcallk(L, 0, c.LUA_MULTRET, 0, 0, null); 132 | if (res != c.LUA_OK) { 133 | std.log.err("Lua lua_pcallk failure {}", .{res}); 134 | return error.LuaInitFailure; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /data/core/commands/findreplace.lua: -------------------------------------------------------------------------------- 1 | local core = require "core" 2 | local command = require "core.command" 3 | local config = require "core.config" 4 | local search = require "core.doc.search" 5 | local DocView = require "core.docview" 6 | 7 | local max_previous_finds = 50 8 | 9 | 10 | local function doc() 11 | return core.active_view.doc 12 | end 13 | 14 | 15 | local previous_finds 16 | local last_doc 17 | local last_fn, last_text 18 | 19 | 20 | local function push_previous_find(doc, sel) 21 | if last_doc ~= doc then 22 | last_doc = doc 23 | previous_finds = {} 24 | end 25 | if #previous_finds >= max_previous_finds then 26 | table.remove(previous_finds, 1) 27 | end 28 | table.insert(previous_finds, sel or { doc:get_selection() }) 29 | end 30 | 31 | 32 | local function find(label, search_fn) 33 | local dv = core.active_view 34 | local sel = { dv.doc:get_selection() } 35 | local text = dv.doc:get_text(table.unpack(sel)) 36 | local found = false 37 | 38 | core.command_view:set_text(text, true) 39 | 40 | core.command_view:enter(label, function(text) 41 | if found then 42 | last_fn, last_text = search_fn, text 43 | previous_finds = {} 44 | push_previous_find(dv.doc, sel) 45 | else 46 | core.error("Couldn't find %q", text) 47 | dv.doc:set_selection(table.unpack(sel)) 48 | dv:scroll_to_make_visible(sel[1], sel[2]) 49 | end 50 | 51 | end, function(text) 52 | local ok, line1, col1, line2, col2 = pcall(search_fn, dv.doc, sel[1], sel[2], text) 53 | if ok and line1 and text ~= "" then 54 | dv.doc:set_selection(line2, col2, line1, col1) 55 | dv:scroll_to_line(line2, true) 56 | found = true 57 | else 58 | dv.doc:set_selection(table.unpack(sel)) 59 | found = false 60 | end 61 | 62 | end, function(explicit) 63 | if explicit then 64 | dv.doc:set_selection(table.unpack(sel)) 65 | dv:scroll_to_make_visible(sel[1], sel[2]) 66 | end 67 | end) 68 | end 69 | 70 | 71 | local function replace(kind, default, fn) 72 | core.command_view:set_text(default, true) 73 | 74 | core.command_view:enter("Find To Replace " .. kind, function(old) 75 | core.command_view:set_text(old, true) 76 | 77 | local s = string.format("Replace %s %q With", kind, old) 78 | core.command_view:enter(s, function(new) 79 | local n = doc():replace(function(text) 80 | return fn(text, old, new) 81 | end) 82 | core.log("Replaced %d instance(s) of %s %q with %q", n, kind, old, new) 83 | end) 84 | end) 85 | end 86 | 87 | 88 | local function has_selection() 89 | return core.active_view:is(DocView) 90 | and core.active_view.doc:has_selection() 91 | end 92 | 93 | command.add(has_selection, { 94 | ["find-replace:select-next"] = function() 95 | local l1, c1, l2, c2 = doc():get_selection(true) 96 | local text = doc():get_text(l1, c1, l2, c2) 97 | local l1, c1, l2, c2 = search.find(doc(), l2, c2, text, { wrap = true }) 98 | if l2 then doc():set_selection(l2, c2, l1, c1) end 99 | end 100 | }) 101 | 102 | command.add("core.docview", { 103 | ["find-replace:find"] = function() 104 | find("Find Text", function(doc, line, col, text) 105 | local opt = { wrap = true, no_case = true } 106 | return search.find(doc, line, col, text, opt) 107 | end) 108 | end, 109 | 110 | ["find-replace:find-pattern"] = function() 111 | find("Find Text Pattern", function(doc, line, col, text) 112 | local opt = { wrap = true, no_case = true, pattern = true } 113 | return search.find(doc, line, col, text, opt) 114 | end) 115 | end, 116 | 117 | ["find-replace:repeat-find"] = function() 118 | if not last_fn then 119 | core.error("No find to continue from") 120 | else 121 | local line, col = doc():get_selection() 122 | local line1, col1, line2, col2 = last_fn(doc(), line, col, last_text) 123 | if line1 then 124 | push_previous_find(doc()) 125 | doc():set_selection(line2, col2, line1, col1) 126 | core.active_view:scroll_to_line(line2, true) 127 | end 128 | end 129 | end, 130 | 131 | ["find-replace:previous-find"] = function() 132 | local sel = table.remove(previous_finds) 133 | if not sel or doc() ~= last_doc then 134 | core.error("No previous finds") 135 | return 136 | end 137 | doc():set_selection(table.unpack(sel)) 138 | core.active_view:scroll_to_line(sel[3], true) 139 | end, 140 | 141 | ["find-replace:replace"] = function() 142 | replace("Text", "", function(text, old, new) 143 | return text:gsub(old:gsub("%W", "%%%1"), new:gsub("%%", "%%%%"), nil) 144 | end) 145 | end, 146 | 147 | ["find-replace:replace-pattern"] = function() 148 | replace("Pattern", "", function(text, old, new) 149 | return text:gsub(old, new) 150 | end) 151 | end, 152 | 153 | ["find-replace:replace-symbol"] = function() 154 | local first = "" 155 | if doc():has_selection() then 156 | local text = doc():get_text(doc():get_selection()) 157 | first = text:match(config.symbol_pattern) or "" 158 | end 159 | replace("Symbol", first, function(text, old, new) 160 | local n = 0 161 | local res = text:gsub(config.symbol_pattern, function(sym) 162 | if old == sym then 163 | n = n + 1 164 | return new 165 | end 166 | end) 167 | return res, n 168 | end) 169 | end, 170 | }) 171 | -------------------------------------------------------------------------------- /data/plugins/language_odin.lua: -------------------------------------------------------------------------------- 1 | local syntax = require "core.syntax" 2 | 3 | syntax.add { 4 | files = "%.odin$", 5 | comment = "//", 6 | patterns = { 7 | { pattern = "//.-\n", type = "comment" }, 8 | { pattern = { "/%*", "%*/" }, type = "comment" }, 9 | { pattern = { '"', '"', '\\' }, type = "string" }, 10 | { pattern = { "'", "'", '\\' }, type = "string" }, 11 | { pattern = { "`", "`" }, type = "string" }, 12 | { pattern = "0b[01_]+", type = "number" }, 13 | { pattern = "0o[0-7_]+", type = "number" }, 14 | { pattern = "0[dz][%d_]+", type = "number" }, 15 | { pattern = "0x[%da-fA-F_]+", type = "number" }, 16 | { pattern = "-?%d+[%d%._e]*i?", type = "number" }, 17 | { pattern = "[<>~=+-*/]=", type = "operator" }, 18 | { pattern = "[%+%-=/%*%^%%<>!~|&:]", type = "operator" }, 19 | { pattern = "%$[%a_][%w_]*", type = "operator" }, 20 | { pattern = "[%a_][%w_]*%f[(]", type = "function" }, 21 | { pattern = "[#@][%a_][%w_]*", type = "keyword2" }, 22 | { pattern = "[#@]%b()", type = "keyword2" }, 23 | { pattern = "[%a_][%w_]*", type = "symbol" }, 24 | }, 25 | symbols = { 26 | -- Keywords 27 | ["package"] = "keyword", 28 | ["import"] = "keyword", 29 | ["foreign"] = "keyword", 30 | ["when"] = "keyword", 31 | ["if"] = "keyword", 32 | ["else"] = "keyword", 33 | ["for"] = "keyword", 34 | ["defer"] = "keyword", 35 | ["return"] = "keyword", 36 | ["switch"] = "keyword", 37 | ["case"] = "keyword", 38 | ["in"] = "keyword", 39 | ["not_in"] = "keyword", 40 | ["do"] = "keyword", 41 | ["break"] = "keyword", 42 | ["continue"] = "keyword", 43 | ["fallthrough"] = "keyword", 44 | ["proc"] = "keyword", 45 | ["struct"] = "keyword", 46 | ["union"] = "keyword", 47 | ["enum"] = "keyword", 48 | ["bit_set"] = "keyword", 49 | ["map"] = "keyword", 50 | ["dynamic"] = "keyword", 51 | ["using"] = "keyword", 52 | ["inline"] = "keyword", 53 | ["no_inline"] = "keyword", 54 | ["context"] = "keyword", 55 | ["distinct"] = "keyword", 56 | ["opaque"] = "keyword", 57 | ["macro"] = "keyword", -- Reserved, not yet used 58 | ["const"] = "keyword", -- Reserved, not yet used 59 | -- Builtin procedures and directives 60 | ["cast"] = "keyword2", 61 | ["auto_cast"] = "keyword2", 62 | ["transmute"] = "keyword2", 63 | ["len"] = "keyword2", 64 | ["cap"] = "keyword2", 65 | ["size_of"] = "keyword2", 66 | ["align_of"] = "keyword2", 67 | ["offset_of"] = "keyword2", 68 | ["typeid_of"] = "keyword2", 69 | ["type_of"] = "keyword2", 70 | ["type_info_of"] = "keyword2", 71 | ["type_info_base"] = "keyword2", 72 | ["swizzle"] = "keyword2", 73 | ["complex"] = "keyword2", 74 | ["real"] = "keyword2", 75 | ["imag"] = "keyword2", 76 | ["conj"] = "keyword2", 77 | ["min"] = "keyword2", 78 | ["max"] = "keyword2", 79 | ["abs"] = "keyword2", 80 | ["clamp"] = "keyword2", 81 | ["assert"] = "keyword2", 82 | -- Types 83 | ["rawptr"] = "keyword2", 84 | ["typeid"] = "keyword2", 85 | ["any"] = "keyword2", 86 | ["string"] = "keyword2", 87 | ["cstring"] = "keyword2", 88 | ["int"] = "keyword2", 89 | ["uint"] = "keyword2", 90 | ["uintptr"] = "keyword2", 91 | ["rune"] = "keyword2", 92 | ["byte"] = "keyword2", 93 | ["u8"] = "keyword2", 94 | ["u16"] = "keyword2", 95 | ["u32"] = "keyword2", 96 | ["u64"] = "keyword2", 97 | ["u128"] = "keyword2", 98 | ["i8"] = "keyword2", 99 | ["i16"] = "keyword2", 100 | ["i32"] = "keyword2", 101 | ["i64"] = "keyword2", 102 | ["i128"] = "keyword2", 103 | ["f16"] = "keyword2", 104 | ["f32"] = "keyword2", 105 | ["f64"] = "keyword2", 106 | ["u16le"] = "keyword2", 107 | ["u32le"] = "keyword2", 108 | ["u64le"] = "keyword2", 109 | ["u128le"] = "keyword2", 110 | ["i16le"] = "keyword2", 111 | ["i32le"] = "keyword2", 112 | ["i64le"] = "keyword2", 113 | ["i128le"] = "keyword2", 114 | ["u16be"] = "keyword2", 115 | ["u32be"] = "keyword2", 116 | ["u64be"] = "keyword2", 117 | ["u128be"] = "keyword2", 118 | ["i16be"] = "keyword2", 119 | ["i32be"] = "keyword2", 120 | ["i64be"] = "keyword2", 121 | ["i128be"] = "keyword2", 122 | ["complex32"] = "keyword2", 123 | ["complex64"] = "keyword2", 124 | ["complex128"] = "keyword2", 125 | ["quaternion128"] = "keyword2", 126 | ["quaternion256"] = "keyword2", 127 | ["bool"] = "keyword2", 128 | ["b8"] = "keyword2", 129 | ["b32"] = "keyword2", 130 | ["b64"] = "keyword2", 131 | ["b128"] = "keyword2", 132 | -- Literals 133 | ["true"] = "literal", 134 | ["false"] = "literal", 135 | ["nil"] = "literal", 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /data/plugins/treeview.lua: -------------------------------------------------------------------------------- 1 | local core = require "core" 2 | local common = require "core.common" 3 | local command = require "core.command" 4 | local config = require "core.config" 5 | local keymap = require "core.keymap" 6 | local style = require "core.style" 7 | local View = require "core.view" 8 | 9 | config.treeview_size = 200 * SCALE 10 | 11 | local function get_depth(filename) 12 | local n = 0 13 | for sep in filename:gmatch("[\\/]") do 14 | n = n + 1 15 | end 16 | return n 17 | end 18 | 19 | 20 | local TreeView = View:extend() 21 | 22 | function TreeView:new() 23 | TreeView.super.new(self) 24 | self.scrollable = true 25 | self.visible = true 26 | self.init_size = true 27 | self.cache = {} 28 | end 29 | 30 | 31 | function TreeView:get_cached(item) 32 | local t = self.cache[item.filename] 33 | if not t then 34 | t = {} 35 | t.filename = item.filename 36 | t.abs_filename = system.absolute_path(item.filename) 37 | t.name = t.filename:match("[^\\/]+$") 38 | t.depth = get_depth(t.filename) 39 | t.type = item.type 40 | self.cache[t.filename] = t 41 | end 42 | return t 43 | end 44 | 45 | 46 | function TreeView:get_name() 47 | return "Project" 48 | end 49 | 50 | 51 | function TreeView:get_item_height() 52 | return style.font:get_height() + style.padding.y 53 | end 54 | 55 | 56 | function TreeView:check_cache() 57 | -- invalidate cache's skip values if project_files has changed 58 | if core.project_files ~= self.last_project_files then 59 | for _, v in pairs(self.cache) do 60 | v.skip = nil 61 | end 62 | self.last_project_files = core.project_files 63 | end 64 | end 65 | 66 | 67 | function TreeView:each_item() 68 | return coroutine.wrap(function() 69 | self:check_cache() 70 | local ox, oy = self:get_content_offset() 71 | local y = oy + style.padding.y 72 | local w = self.size.x 73 | local h = self:get_item_height() 74 | 75 | local i = 1 76 | while i <= #core.project_files do 77 | local item = core.project_files[i] 78 | local cached = self:get_cached(item) 79 | 80 | coroutine.yield(cached, ox, y, w, h) 81 | y = y + h 82 | i = i + 1 83 | 84 | if not cached.expanded then 85 | if cached.skip then 86 | i = cached.skip 87 | else 88 | local depth = cached.depth 89 | while i <= #core.project_files do 90 | local filename = core.project_files[i].filename 91 | if get_depth(filename) <= depth then break end 92 | i = i + 1 93 | end 94 | cached.skip = i 95 | end 96 | end 97 | end 98 | end) 99 | end 100 | 101 | 102 | function TreeView:on_mouse_moved(px, py) 103 | self.hovered_item = nil 104 | for item, x,y,w,h in self:each_item() do 105 | if px > x and py > y and px <= x + w and py <= y + h then 106 | self.hovered_item = item 107 | break 108 | end 109 | end 110 | end 111 | 112 | 113 | function TreeView:on_mouse_pressed(button, x, y) 114 | if not self.hovered_item then 115 | return 116 | elseif self.hovered_item.type == "dir" then 117 | self.hovered_item.expanded = not self.hovered_item.expanded 118 | else 119 | core.try(function() 120 | core.root_view:open_doc(core.open_doc(self.hovered_item.filename)) 121 | end) 122 | end 123 | end 124 | 125 | 126 | function TreeView:update() 127 | -- update width 128 | local dest = self.visible and config.treeview_size or 0 129 | if self.init_size then 130 | self.size.x = dest 131 | self.init_size = false 132 | else 133 | self:move_towards(self.size, "x", dest) 134 | end 135 | 136 | TreeView.super.update(self) 137 | end 138 | 139 | 140 | function TreeView:draw() 141 | self:draw_background(style.background2) 142 | 143 | local icon_width = style.icon_font:get_width("D") 144 | local spacing = style.font:get_width(" ") * 2 145 | 146 | local doc = core.active_view.doc 147 | local active_filename = doc and system.absolute_path(doc.filename or "") 148 | 149 | for item, x,y,w,h in self:each_item() do 150 | local color = style.text 151 | 152 | -- highlight active_view doc 153 | if item.abs_filename == active_filename then 154 | color = style.accent 155 | end 156 | 157 | -- hovered item background 158 | if item == self.hovered_item then 159 | renderer.draw_rect(x, y, w, h, style.line_highlight) 160 | color = style.accent 161 | end 162 | 163 | -- icons 164 | x = x + item.depth * style.padding.x + style.padding.x 165 | if item.type == "dir" then 166 | local icon1 = item.expanded and "-" or "+" 167 | local icon2 = item.expanded and "D" or "d" 168 | common.draw_text(style.icon_font, color, icon1, nil, x, y, 0, h) 169 | x = x + style.padding.x 170 | common.draw_text(style.icon_font, color, icon2, nil, x, y, 0, h) 171 | x = x + icon_width 172 | else 173 | x = x + style.padding.x 174 | common.draw_text(style.icon_font, color, "f", nil, x, y, 0, h) 175 | x = x + icon_width 176 | end 177 | 178 | -- text 179 | x = x + spacing 180 | x = common.draw_text(style.font, color, item.name, nil, x, y, 0, h) 181 | end 182 | end 183 | 184 | 185 | -- init 186 | local view = TreeView() 187 | local node = core.root_view:get_active_node() 188 | node:split("left", view, true) 189 | 190 | -- register commands and keymap 191 | command.add(nil, { 192 | ["treeview:toggle"] = function() 193 | view.visible = not view.visible 194 | end, 195 | }) 196 | 197 | keymap.add { ["ctrl+\\"] = "treeview:toggle" } 198 | -------------------------------------------------------------------------------- /main.odin: -------------------------------------------------------------------------------- 1 | package main 2 | import "base:runtime" 3 | import "core:c/libc" 4 | import "core:terminal/ansi" 5 | import "core:fmt" 6 | import "core:mem" 7 | import "core:os" 8 | import "core:os/os2" 9 | import "core:strings" 10 | import "core:dynlib" 11 | 12 | import lua "vendor:lua/5.4" 13 | import sdl "vendor:sdl2" 14 | 15 | _ :: mem 16 | _ :: dynlib 17 | 18 | // global tracking allocator to be used in atexit handler 19 | when ODIN_DEBUG { 20 | track: mem.Tracking_Allocator 21 | } 22 | 23 | window: ^sdl.Window 24 | 25 | get_exe_filename :: proc() -> cstring { 26 | info, err := os2.current_process_info( 27 | os2.Process_Info_Fields{os2.Process_Info_Field.Executable_Path}, 28 | context.temp_allocator, 29 | ) 30 | defer os2.free_process_info(info, context.temp_allocator) 31 | if err != nil { 32 | return "./main" 33 | } 34 | return strings.clone_to_cstring(info.executable_path, context.temp_allocator) 35 | } 36 | 37 | get_scale :: proc() -> f64 { 38 | dpi: f32 39 | _ = sdl.GetDisplayDPI(0, nil, &dpi, nil) 40 | when ODIN_OS == .Windows || ODIN_OS == .Linux{ 41 | return f64(dpi) / 96.0 42 | } else when ODIN_OS == .Darwin { 43 | return f64(dpi) / 72.0 44 | } 45 | return 1.0 46 | } 47 | 48 | init_window_icon :: proc() { 49 | when ODIN_OS != .Windows { 50 | surf: ^sdl.Surface = sdl.CreateRGBSurfaceFrom( 51 | raw_data(icon_rgba[:]), 64, 64, 52 | 32, 64 * 4, 53 | 0x000000ff, 54 | 0x0000ff00, 55 | 0x00ff0000, 56 | 0xff000000) 57 | defer sdl.FreeSurface(surf) 58 | sdl.SetWindowIcon(window, surf) 59 | } 60 | } 61 | 62 | blue :: #force_inline proc($s: string) -> string { 63 | return ansi.CSI + ansi.FG_BLUE + ansi.SGR + s + ansi.CSI + ansi.RESET + ansi.SGR 64 | } 65 | 66 | run_at_exit :: proc "c" () { 67 | context = runtime.default_context() 68 | 69 | when ODIN_DEBUG { 70 | red :: proc($s: string) -> string { 71 | return ansi.CSI + ansi.FG_BRIGHT_RED + ansi.SGR + s + ansi.CSI + ansi.RESET + ansi.SGR 72 | } 73 | 74 | if len(track.allocation_map) > 0 { 75 | fmt.eprintf(red("=== %v allocations not freed: ===\n"), len(track.allocation_map)) 76 | for _, entry in track.allocation_map { 77 | fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location) 78 | } 79 | } 80 | if len(track.bad_free_array) > 0 { 81 | fmt.eprintf(red("=== %v incorrect frees: ===\n"), len(track.bad_free_array)) 82 | for entry in track.bad_free_array { 83 | fmt.eprintf("- %p @ %v\n", entry.memory, entry.location) 84 | } 85 | } 86 | mem.tracking_allocator_destroy(&track) 87 | } 88 | } 89 | 90 | main :: proc() { 91 | when ODIN_OS == .Windows { 92 | lib, ok := dynlib.load_library("user32.dll") 93 | if !ok { 94 | fmt.eprintln(dynlib.last_error()) 95 | } 96 | SetProcessDPIAware, found := dynlib.symbol_address(lib, "a") 97 | if !found { 98 | fmt.eprintln(dynlib.last_error()) 99 | } else { 100 | (cast(proc() -> libc.int)SetProcessDPIAware)() 101 | } 102 | } 103 | 104 | fmt.println(blue("is ODIN_DEBUG: "), ODIN_DEBUG) 105 | when ODIN_DEBUG { 106 | mem.tracking_allocator_init(&track, context.allocator) 107 | context.allocator = mem.tracking_allocator(&track) 108 | // no defer, lua just exits the app 109 | } 110 | 111 | if sdl.Init(sdl.INIT_VIDEO) != 0 { 112 | fmt.println("Unable to get SDL_GetCurrentDisplayMode: %s\n", sdl.GetError()) 113 | return 114 | } 115 | libc.atexit(run_at_exit) 116 | 117 | sdl.EnableScreenSaver() 118 | // ret value can be ignored as it just returns the previous state 119 | sdl.EventState(sdl.EventType.DROPFILE, sdl.ENABLE) 120 | 121 | sdl.SetHint(sdl.HINT_MOUSE_FOCUS_CLICKTHROUGH, "1") 122 | sdl.SetHint(sdl.HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0") 123 | 124 | dm: sdl.DisplayMode 125 | if (sdl.GetCurrentDisplayMode(0, &dm) != 0) { 126 | sdl.Log("Unable to get SDL_GetCurrentDisplayMode: %s\n", sdl.GetError()) 127 | } 128 | 129 | window = sdl.CreateWindow( 130 | "", 131 | sdl.WINDOWPOS_UNDEFINED, 132 | sdl.WINDOWPOS_UNDEFINED, 133 | cast(i32)(cast(f32)dm.w * 0.8), 134 | cast(i32)(cast(f32)dm.h * 0.8), 135 | sdl.WINDOW_RESIZABLE | sdl.WINDOW_ALLOW_HIGHDPI | sdl.WINDOW_HIDDEN, 136 | ) 137 | 138 | init_window_icon() 139 | ren_init(window) 140 | 141 | L := lua.L_newstate() 142 | 143 | lua.L_openlibs(L) 144 | 145 | api_load_libs(L) 146 | 147 | lua.newtable(L) 148 | for arg, idx in os.args { 149 | lua.pushstring(L, strings.clone_to_cstring(arg, context.temp_allocator)) 150 | lua.rawseti(L, -2, cast(lua.Integer)(idx + 1)) 151 | } 152 | lua.setglobal(L, "ARGS") 153 | 154 | lua.pushstring(L, "1.11") 155 | lua.setglobal(L, "VERSION") 156 | 157 | lua.pushstring(L, sdl.GetPlatform()) 158 | lua.setglobal(L, "PLATFORM") 159 | 160 | lua.pushnumber(L, cast(lua.Number)get_scale()) 161 | lua.setglobal(L, "SCALE") 162 | 163 | lua.pushstring(L, get_exe_filename()) 164 | lua.setglobal(L, "EXEFILE") 165 | 166 | lua_code :: `local core 167 | xpcall(function() 168 | SCALE = tonumber(os.getenv("LITE_SCALE")) or SCALE 169 | PATHSEP = package.config:sub(1, 1) 170 | EXEDIR = EXEFILE:match("^(.+)[/\\].*$") 171 | package.path = EXEDIR .. '/data/?.lua;' .. package.path 172 | package.path = EXEDIR .. '/data/?/init.lua;' .. package.path 173 | core = require('core') 174 | core.init() 175 | core.run() 176 | end, function(err) 177 | print('Error: ' .. tostring(err)) 178 | print(debug.traceback(nil, 2)) 179 | if core and core.on_error then 180 | pcall(core.on_error, err) 181 | end 182 | os.exit(1) 183 | end)` 184 | 185 | lua.L_dostring(L, lua_code) 186 | 187 | lua.close(L) 188 | ren_free_fonts() 189 | sdl.DestroyWindow(window) 190 | sdl.Quit() 191 | } 192 | 193 | -------------------------------------------------------------------------------- /data/core/keymap.lua: -------------------------------------------------------------------------------- 1 | local command = require "core.command" 2 | local keymap = {} 3 | 4 | keymap.modkeys = {} 5 | keymap.map = {} 6 | keymap.reverse_map = {} 7 | 8 | local modkey_map = { 9 | ["left ctrl"] = "ctrl", 10 | ["right ctrl"] = "ctrl", 11 | ["left shift"] = "shift", 12 | ["right shift"] = "shift", 13 | ["left alt"] = "alt", 14 | ["right alt"] = "altgr", 15 | } 16 | 17 | local modkeys = { "ctrl", "alt", "altgr", "shift" } 18 | 19 | local function key_to_stroke(k) 20 | local stroke = "" 21 | for _, mk in ipairs(modkeys) do 22 | if keymap.modkeys[mk] then 23 | stroke = stroke .. mk .. "+" 24 | end 25 | end 26 | return stroke .. k 27 | end 28 | 29 | 30 | function keymap.add(map, overwrite) 31 | for stroke, commands in pairs(map) do 32 | if type(commands) == "string" then 33 | commands = { commands } 34 | end 35 | if overwrite then 36 | keymap.map[stroke] = commands 37 | else 38 | keymap.map[stroke] = keymap.map[stroke] or {} 39 | for i = #commands, 1, -1 do 40 | table.insert(keymap.map[stroke], 1, commands[i]) 41 | end 42 | end 43 | for _, cmd in ipairs(commands) do 44 | keymap.reverse_map[cmd] = stroke 45 | end 46 | end 47 | end 48 | 49 | 50 | function keymap.get_binding(cmd) 51 | return keymap.reverse_map[cmd] 52 | end 53 | 54 | 55 | function keymap.on_key_pressed(k) 56 | local mk = modkey_map[k] 57 | if mk then 58 | keymap.modkeys[mk] = true 59 | -- work-around for windows where `altgr` is treated as `ctrl+alt` 60 | if mk == "altgr" then 61 | keymap.modkeys["ctrl"] = false 62 | end 63 | else 64 | local stroke = key_to_stroke(k) 65 | local commands = keymap.map[stroke] 66 | if commands then 67 | for _, cmd in ipairs(commands) do 68 | local performed = command.perform(cmd) 69 | if performed then break end 70 | end 71 | return true 72 | end 73 | end 74 | return false 75 | end 76 | 77 | 78 | function keymap.on_key_released(k) 79 | local mk = modkey_map[k] 80 | if mk then 81 | keymap.modkeys[mk] = false 82 | end 83 | end 84 | 85 | 86 | keymap.add { 87 | ["ctrl+shift+p"] = "core:find-command", 88 | ["ctrl+p"] = "core:find-file", 89 | ["ctrl+o"] = "core:open-file", 90 | ["ctrl+n"] = "core:new-doc", 91 | ["alt+return"] = "core:toggle-fullscreen", 92 | 93 | ["alt+shift+j"] = "root:split-left", 94 | ["alt+shift+l"] = "root:split-right", 95 | ["alt+shift+i"] = "root:split-up", 96 | ["alt+shift+k"] = "root:split-down", 97 | ["alt+j"] = "root:switch-to-left", 98 | ["alt+l"] = "root:switch-to-right", 99 | ["alt+i"] = "root:switch-to-up", 100 | ["alt+k"] = "root:switch-to-down", 101 | 102 | ["ctrl+w"] = "root:close", 103 | ["ctrl+tab"] = "root:switch-to-next-tab", 104 | ["ctrl+shift+tab"] = "root:switch-to-previous-tab", 105 | ["ctrl+pageup"] = "root:move-tab-left", 106 | ["ctrl+pagedown"] = "root:move-tab-right", 107 | ["alt+1"] = "root:switch-to-tab-1", 108 | ["alt+2"] = "root:switch-to-tab-2", 109 | ["alt+3"] = "root:switch-to-tab-3", 110 | ["alt+4"] = "root:switch-to-tab-4", 111 | ["alt+5"] = "root:switch-to-tab-5", 112 | ["alt+6"] = "root:switch-to-tab-6", 113 | ["alt+7"] = "root:switch-to-tab-7", 114 | ["alt+8"] = "root:switch-to-tab-8", 115 | ["alt+9"] = "root:switch-to-tab-9", 116 | 117 | ["ctrl+f"] = "find-replace:find", 118 | ["ctrl+r"] = "find-replace:replace", 119 | ["f3"] = "find-replace:repeat-find", 120 | ["shift+f3"] = "find-replace:previous-find", 121 | ["ctrl+g"] = "doc:go-to-line", 122 | ["ctrl+s"] = "doc:save", 123 | ["ctrl+shift+s"] = "doc:save-as", 124 | 125 | ["ctrl+z"] = "doc:undo", 126 | ["ctrl+y"] = "doc:redo", 127 | ["ctrl+x"] = "doc:cut", 128 | ["ctrl+c"] = "doc:copy", 129 | ["ctrl+v"] = "doc:paste", 130 | ["escape"] = { "command:escape", "doc:select-none" }, 131 | ["tab"] = { "command:complete", "doc:indent" }, 132 | ["shift+tab"] = "doc:unindent", 133 | ["backspace"] = "doc:backspace", 134 | ["shift+backspace"] = "doc:backspace", 135 | ["ctrl+backspace"] = "doc:delete-to-previous-word-start", 136 | ["ctrl+shift+backspace"] = "doc:delete-to-previous-word-start", 137 | ["delete"] = "doc:delete", 138 | ["shift+delete"] = "doc:delete", 139 | ["ctrl+delete"] = "doc:delete-to-next-word-end", 140 | ["ctrl+shift+delete"] = "doc:delete-to-next-word-end", 141 | ["return"] = { "command:submit", "doc:newline" }, 142 | ["keypad enter"] = { "command:submit", "doc:newline" }, 143 | ["ctrl+return"] = "doc:newline-below", 144 | ["ctrl+shift+return"] = "doc:newline-above", 145 | ["ctrl+j"] = "doc:join-lines", 146 | ["ctrl+a"] = "doc:select-all", 147 | ["ctrl+d"] = { "find-replace:select-next", "doc:select-word" }, 148 | ["ctrl+l"] = "doc:select-lines", 149 | ["ctrl+/"] = "doc:toggle-line-comments", 150 | ["ctrl+up"] = "doc:move-lines-up", 151 | ["ctrl+down"] = "doc:move-lines-down", 152 | ["ctrl+shift+d"] = "doc:duplicate-lines", 153 | ["ctrl+shift+k"] = "doc:delete-lines", 154 | 155 | ["left"] = "doc:move-to-previous-char", 156 | ["right"] = "doc:move-to-next-char", 157 | ["up"] = { "command:select-previous", "doc:move-to-previous-line" }, 158 | ["down"] = { "command:select-next", "doc:move-to-next-line" }, 159 | ["ctrl+left"] = "doc:move-to-previous-word-start", 160 | ["ctrl+right"] = "doc:move-to-next-word-end", 161 | ["ctrl+["] = "doc:move-to-previous-block-start", 162 | ["ctrl+]"] = "doc:move-to-next-block-end", 163 | ["home"] = "doc:move-to-start-of-line", 164 | ["end"] = "doc:move-to-end-of-line", 165 | ["ctrl+home"] = "doc:move-to-start-of-doc", 166 | ["ctrl+end"] = "doc:move-to-end-of-doc", 167 | ["pageup"] = "doc:move-to-previous-page", 168 | ["pagedown"] = "doc:move-to-next-page", 169 | 170 | ["shift+left"] = "doc:select-to-previous-char", 171 | ["shift+right"] = "doc:select-to-next-char", 172 | ["shift+up"] = "doc:select-to-previous-line", 173 | ["shift+down"] = "doc:select-to-next-line", 174 | ["ctrl+shift+left"] = "doc:select-to-previous-word-start", 175 | ["ctrl+shift+right"] = "doc:select-to-next-word-end", 176 | ["ctrl+shift+["] = "doc:select-to-previous-block-start", 177 | ["ctrl+shift+]"] = "doc:select-to-next-block-end", 178 | ["shift+home"] = "doc:select-to-start-of-line", 179 | ["shift+end"] = "doc:select-to-end-of-line", 180 | ["ctrl+shift+home"] = "doc:select-to-start-of-doc", 181 | ["ctrl+shift+end"] = "doc:select-to-end-of-doc", 182 | ["shift+pageup"] = "doc:select-to-previous-page", 183 | ["shift+pagedown"] = "doc:select-to-next-page", 184 | } 185 | 186 | return keymap 187 | -------------------------------------------------------------------------------- /data/core/commandview.lua: -------------------------------------------------------------------------------- 1 | local core = require "core" 2 | local common = require "core.common" 3 | local style = require "core.style" 4 | local Doc = require "core.doc" 5 | local DocView = require "core.docview" 6 | local View = require "core.view" 7 | 8 | 9 | local SingleLineDoc = Doc:extend() 10 | 11 | function SingleLineDoc:insert(line, col, text) 12 | SingleLineDoc.super.insert(self, line, col, text:gsub("\n", "")) 13 | end 14 | 15 | 16 | local CommandView = DocView:extend() 17 | 18 | local max_suggestions = 10 19 | 20 | local noop = function() end 21 | 22 | local default_state = { 23 | submit = noop, 24 | suggest = noop, 25 | cancel = noop, 26 | } 27 | 28 | 29 | function CommandView:new() 30 | CommandView.super.new(self, SingleLineDoc()) 31 | self.suggestion_idx = 1 32 | self.suggestions = {} 33 | self.suggestions_height = 0 34 | self.last_change_id = 0 35 | self.gutter_width = 0 36 | self.gutter_text_brightness = 0 37 | self.selection_offset = 0 38 | self.state = default_state 39 | self.font = "font" 40 | self.size.y = 0 41 | self.label = "" 42 | end 43 | 44 | 45 | function CommandView:get_name() 46 | return View.get_name(self) 47 | end 48 | 49 | 50 | function CommandView:get_line_screen_position() 51 | local x = CommandView.super.get_line_screen_position(self, 1) 52 | local _, y = self:get_content_offset() 53 | local lh = self:get_line_height() 54 | return x, y + (self.size.y - lh) / 2 55 | end 56 | 57 | 58 | function CommandView:get_scrollable_size() 59 | return 0 60 | end 61 | 62 | 63 | function CommandView:scroll_to_make_visible() 64 | -- no-op function to disable this functionality 65 | end 66 | 67 | 68 | function CommandView:get_text() 69 | return self.doc:get_text(1, 1, 1, math.huge) 70 | end 71 | 72 | 73 | function CommandView:set_text(text, select) 74 | self.doc:remove(1, 1, math.huge, math.huge) 75 | self.doc:text_input(text) 76 | if select then 77 | self.doc:set_selection(math.huge, math.huge, 1, 1) 78 | end 79 | end 80 | 81 | 82 | function CommandView:move_suggestion_idx(dir) 83 | local n = self.suggestion_idx + dir 84 | self.suggestion_idx = common.clamp(n, 1, #self.suggestions) 85 | self:complete() 86 | self.last_change_id = self.doc:get_change_id() 87 | end 88 | 89 | 90 | function CommandView:complete() 91 | if #self.suggestions > 0 then 92 | self:set_text(self.suggestions[self.suggestion_idx].text) 93 | end 94 | end 95 | 96 | 97 | function CommandView:submit() 98 | local suggestion = self.suggestions[self.suggestion_idx] 99 | local text = self:get_text() 100 | local submit = self.state.submit 101 | self:exit(true) 102 | submit(text, suggestion) 103 | end 104 | 105 | 106 | function CommandView:enter(text, submit, suggest, cancel) 107 | if self.state ~= default_state then 108 | return 109 | end 110 | self.state = { 111 | submit = submit or noop, 112 | suggest = suggest or noop, 113 | cancel = cancel or noop, 114 | } 115 | core.set_active_view(self) 116 | self:update_suggestions() 117 | self.gutter_text_brightness = 100 118 | self.label = text .. ": " 119 | end 120 | 121 | 122 | function CommandView:exit(submitted, inexplicit) 123 | if core.active_view == self then 124 | core.set_active_view(core.last_active_view) 125 | end 126 | local cancel = self.state.cancel 127 | self.state = default_state 128 | self.doc:reset() 129 | self.suggestions = {} 130 | if not submitted then cancel(not inexplicit) end 131 | end 132 | 133 | 134 | function CommandView:get_gutter_width() 135 | return self.gutter_width 136 | end 137 | 138 | 139 | function CommandView:get_suggestion_line_height() 140 | return self:get_font():get_height() + style.padding.y 141 | end 142 | 143 | 144 | function CommandView:update_suggestions() 145 | local t = self.state.suggest(self:get_text()) or {} 146 | local res = {} 147 | for i, item in ipairs(t) do 148 | if i == max_suggestions then 149 | break 150 | end 151 | if type(item) == "string" then 152 | item = { text = item } 153 | end 154 | res[i] = item 155 | end 156 | self.suggestions = res 157 | self.suggestion_idx = 1 158 | end 159 | 160 | 161 | function CommandView:update() 162 | CommandView.super.update(self) 163 | 164 | if core.active_view ~= self and self.state ~= default_state then 165 | self:exit(false, true) 166 | end 167 | 168 | -- update suggestions if text has changed 169 | if self.last_change_id ~= self.doc:get_change_id() then 170 | self:update_suggestions() 171 | self.last_change_id = self.doc:get_change_id() 172 | end 173 | 174 | -- update gutter text color brightness 175 | self:move_towards("gutter_text_brightness", 0, 0.1) 176 | 177 | -- update gutter width 178 | local dest = self:get_font():get_width(self.label) + style.padding.x 179 | if self.size.y <= 0 then 180 | self.gutter_width = dest 181 | else 182 | self:move_towards("gutter_width", dest) 183 | end 184 | 185 | -- update suggestions box height 186 | local lh = self:get_suggestion_line_height() 187 | local dest = #self.suggestions * lh 188 | self:move_towards("suggestions_height", dest) 189 | 190 | -- update suggestion cursor offset 191 | local dest = self.suggestion_idx * self:get_suggestion_line_height() 192 | self:move_towards("selection_offset", dest) 193 | 194 | -- update size based on whether this is the active_view 195 | local dest = 0 196 | if self == core.active_view then 197 | dest = style.font:get_height() + style.padding.y * 2 198 | end 199 | self:move_towards(self.size, "y", dest) 200 | end 201 | 202 | 203 | function CommandView:draw_line_highlight() 204 | -- no-op function to disable this functionality 205 | end 206 | 207 | 208 | function CommandView:draw_line_gutter(idx, x, y) 209 | local yoffset = self:get_line_text_y_offset() 210 | local pos = self.position 211 | local color = common.lerp(style.text, style.accent, self.gutter_text_brightness / 100) 212 | core.push_clip_rect(pos.x, pos.y, self:get_gutter_width(), self.size.y) 213 | x = x + style.padding.x 214 | renderer.draw_text(self:get_font(), self.label, x, y + yoffset, color) 215 | core.pop_clip_rect() 216 | end 217 | 218 | 219 | local function draw_suggestions_box(self) 220 | local lh = self:get_suggestion_line_height() 221 | local dh = style.divider_size 222 | local x, _ = self:get_line_screen_position() 223 | local h = math.ceil(self.suggestions_height) 224 | local rx, ry, rw, rh = self.position.x, self.position.y - h - dh, self.size.x, h 225 | 226 | -- draw suggestions background 227 | if #self.suggestions > 0 then 228 | renderer.draw_rect(rx, ry, rw, rh, style.background3) 229 | renderer.draw_rect(rx, ry - dh, rw, dh, style.divider) 230 | local y = self.position.y - self.selection_offset - dh 231 | renderer.draw_rect(rx, y, rw, lh, style.line_highlight) 232 | end 233 | 234 | -- draw suggestion text 235 | core.push_clip_rect(rx, ry, rw, rh) 236 | for i, item in ipairs(self.suggestions) do 237 | local color = (i == self.suggestion_idx) and style.accent or style.text 238 | local y = self.position.y - i * lh - dh 239 | common.draw_text(self:get_font(), color, item.text, nil, x, y, 0, lh) 240 | 241 | if item.info then 242 | local w = self.size.x - x - style.padding.x 243 | common.draw_text(self:get_font(), style.dim, item.info, "right", x, y, w, lh) 244 | end 245 | end 246 | core.pop_clip_rect() 247 | end 248 | 249 | 250 | function CommandView:draw() 251 | CommandView.super.draw(self) 252 | core.root_view:defer_draw(draw_suggestions_box, self) 253 | end 254 | 255 | 256 | return CommandView 257 | -------------------------------------------------------------------------------- /data/plugins/autocomplete.lua: -------------------------------------------------------------------------------- 1 | local core = require "core" 2 | local common = require "core.common" 3 | local config = require "core.config" 4 | local command = require "core.command" 5 | local style = require "core.style" 6 | local keymap = require "core.keymap" 7 | local translate = require "core.doc.translate" 8 | local RootView = require "core.rootview" 9 | local DocView = require "core.docview" 10 | 11 | config.autocomplete_max_suggestions = 6 12 | 13 | local autocomplete = {} 14 | autocomplete.map = {} 15 | 16 | 17 | local mt = { __tostring = function(t) return t.text end } 18 | 19 | function autocomplete.add(t) 20 | local items = {} 21 | for text, info in pairs(t.items) do 22 | info = (type(info) == "string") and info 23 | table.insert(items, setmetatable({ text = text, info = info }, mt)) 24 | end 25 | autocomplete.map[t.name] = { files = t.files or ".*", items = items } 26 | end 27 | 28 | 29 | core.add_thread(function() 30 | local cache = setmetatable({}, { __mode = "k" }) 31 | 32 | local function get_symbols(doc) 33 | local i = 1 34 | local s = {} 35 | while i < #doc.lines do 36 | for sym in doc.lines[i]:gmatch(config.symbol_pattern) do 37 | s[sym] = true 38 | end 39 | i = i + 1 40 | if i % 100 == 0 then coroutine.yield() end 41 | end 42 | return s 43 | end 44 | 45 | local function cache_is_valid(doc) 46 | local c = cache[doc] 47 | return c and c.last_change_id == doc:get_change_id() 48 | end 49 | 50 | while true do 51 | local symbols = {} 52 | 53 | -- lift all symbols from all docs 54 | for _, doc in ipairs(core.docs) do 55 | -- update the cache if the doc has changed since the last iteration 56 | if not cache_is_valid(doc) then 57 | cache[doc] = { 58 | last_change_id = doc:get_change_id(), 59 | symbols = get_symbols(doc) 60 | } 61 | end 62 | -- update symbol set with doc's symbol set 63 | for sym in pairs(cache[doc].symbols) do 64 | symbols[sym] = true 65 | end 66 | coroutine.yield() 67 | end 68 | 69 | -- update symbols list 70 | autocomplete.add { name = "open-docs", items = symbols } 71 | 72 | -- wait for next scan 73 | local valid = true 74 | while valid do 75 | coroutine.yield(1) 76 | for _, doc in ipairs(core.docs) do 77 | if not cache_is_valid(doc) then 78 | valid = false 79 | end 80 | end 81 | end 82 | 83 | end 84 | end) 85 | 86 | 87 | local partial = "" 88 | local suggestions_idx = 1 89 | local suggestions = {} 90 | local last_line, last_col 91 | 92 | 93 | local function reset_suggestions() 94 | suggestions_idx = 1 95 | suggestions = {} 96 | end 97 | 98 | 99 | local function update_suggestions() 100 | local doc = core.active_view.doc 101 | local filename = doc and doc.filename or "" 102 | 103 | -- get all relevant suggestions for given filename 104 | local items = {} 105 | for _, v in pairs(autocomplete.map) do 106 | if common.match_pattern(filename, v.files) then 107 | for _, item in pairs(v.items) do 108 | table.insert(items, item) 109 | end 110 | end 111 | end 112 | 113 | -- fuzzy match, remove duplicates and store 114 | items = common.fuzzy_match(items, partial) 115 | local j = 1 116 | for i = 1, config.autocomplete_max_suggestions do 117 | suggestions[i] = items[j] 118 | while items[j] and items[i].text == items[j].text do 119 | items[i].info = items[i].info or items[j].info 120 | j = j + 1 121 | end 122 | end 123 | end 124 | 125 | 126 | local function get_partial_symbol() 127 | local doc = core.active_view.doc 128 | local line2, col2 = doc:get_selection() 129 | local line1, col1 = doc:position_offset(line2, col2, translate.start_of_word) 130 | return doc:get_text(line1, col1, line2, col2) 131 | end 132 | 133 | 134 | local function get_active_view() 135 | if getmetatable(core.active_view) == DocView then 136 | return core.active_view 137 | end 138 | end 139 | 140 | 141 | local function get_suggestions_rect(av) 142 | if #suggestions == 0 then 143 | return 0, 0, 0, 0 144 | end 145 | 146 | local line, col = av.doc:get_selection() 147 | local x, y = av:get_line_screen_position(line) 148 | x = x + av:get_col_x_offset(line, col - #partial) 149 | y = y + av:get_line_height() + style.padding.y 150 | local font = av:get_font() 151 | local th = font:get_height() 152 | 153 | local max_width = 0 154 | for _, s in ipairs(suggestions) do 155 | local w = font:get_width(s.text) 156 | if s.info then 157 | w = w + style.font:get_width(s.info) + style.padding.x 158 | end 159 | max_width = math.max(max_width, w) 160 | end 161 | 162 | return 163 | x - style.padding.x, 164 | y - style.padding.y, 165 | max_width + style.padding.x * 2, 166 | #suggestions * (th + style.padding.y) + style.padding.y 167 | end 168 | 169 | 170 | local function draw_suggestions_box(av) 171 | -- draw background rect 172 | local rx, ry, rw, rh = get_suggestions_rect(av) 173 | renderer.draw_rect(rx, ry, rw, rh, style.background3) 174 | 175 | -- draw text 176 | local font = av:get_font() 177 | local lh = font:get_height() + style.padding.y 178 | local y = ry + style.padding.y / 2 179 | for i, s in ipairs(suggestions) do 180 | local color = (i == suggestions_idx) and style.accent or style.text 181 | common.draw_text(font, color, s.text, "left", rx + style.padding.x, y, rw, lh) 182 | if s.info then 183 | color = (i == suggestions_idx) and style.text or style.dim 184 | common.draw_text(style.font, color, s.info, "right", rx, y, rw - style.padding.x, lh) 185 | end 186 | y = y + lh 187 | end 188 | end 189 | 190 | 191 | -- patch event logic into RootView 192 | local on_text_input = RootView.on_text_input 193 | local update = RootView.update 194 | local draw = RootView.draw 195 | 196 | 197 | RootView.on_text_input = function(...) 198 | on_text_input(...) 199 | 200 | local av = get_active_view() 201 | if av then 202 | -- update partial symbol and suggestions 203 | partial = get_partial_symbol() 204 | if #partial >= 3 then 205 | update_suggestions() 206 | last_line, last_col = av.doc:get_selection() 207 | else 208 | reset_suggestions() 209 | end 210 | 211 | -- scroll if rect is out of bounds of view 212 | local _, y, _, h = get_suggestions_rect(av) 213 | local limit = av.position.y + av.size.y 214 | if y + h > limit then 215 | av.scroll.to.y = av.scroll.y + y + h - limit 216 | end 217 | end 218 | end 219 | 220 | 221 | RootView.update = function(...) 222 | update(...) 223 | 224 | local av = get_active_view() 225 | if av then 226 | -- reset suggestions if caret was moved 227 | local line, col = av.doc:get_selection() 228 | if line ~= last_line or col ~= last_col then 229 | reset_suggestions() 230 | end 231 | end 232 | end 233 | 234 | 235 | RootView.draw = function(...) 236 | draw(...) 237 | 238 | local av = get_active_view() 239 | if av then 240 | -- draw suggestions box after everything else 241 | core.root_view:defer_draw(draw_suggestions_box, av) 242 | end 243 | end 244 | 245 | 246 | local function predicate() 247 | return get_active_view() and #suggestions > 0 248 | end 249 | 250 | 251 | command.add(predicate, { 252 | ["autocomplete:complete"] = function() 253 | local doc = core.active_view.doc 254 | local line, col = doc:get_selection() 255 | local text = suggestions[suggestions_idx].text 256 | doc:insert(line, col, text) 257 | doc:remove(line, col, line, col - #partial) 258 | doc:set_selection(line, col + #text - #partial) 259 | reset_suggestions() 260 | end, 261 | 262 | ["autocomplete:previous"] = function() 263 | suggestions_idx = math.max(suggestions_idx - 1, 1) 264 | end, 265 | 266 | ["autocomplete:next"] = function() 267 | suggestions_idx = math.min(suggestions_idx + 1, #suggestions) 268 | end, 269 | 270 | ["autocomplete:cancel"] = function() 271 | reset_suggestions() 272 | end, 273 | }) 274 | 275 | 276 | keymap.add { 277 | ["tab"] = "autocomplete:complete", 278 | ["up"] = "autocomplete:previous", 279 | ["down"] = "autocomplete:next", 280 | ["escape"] = "autocomplete:cancel", 281 | } 282 | 283 | 284 | return autocomplete 285 | -------------------------------------------------------------------------------- /rencache.odin: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "base:runtime" 4 | import "core:fmt" 5 | import "core:math" 6 | import "core:math/rand" 7 | import "core:mem" 8 | import "core:strings" 9 | 10 | /* a cache over the software renderer -- all drawing operations are stored as 11 | ** commands when issued. At the end of the frame we write the commands to a grid 12 | ** of hash values, take the cells that have changed since the previous frame, 13 | ** merge them into dirty rectangles and redraw only those regions */ 14 | 15 | CELLS_X :: 80 16 | CELLS_Y :: 50 17 | CELL_SIZE :: 96 18 | COMMAND_BUF_SIZE :: 1024 * 512 19 | 20 | HASH_INITIAL :: 2166136261 21 | 22 | @(private) 23 | CommandType :: enum { 24 | FREE_FONT, 25 | SET_CLIP, 26 | DRAW_TEXT, 27 | DRAW_RECT, 28 | } 29 | 30 | Command :: struct { 31 | type: CommandType, 32 | size: i32, 33 | rect: RenRect, 34 | color: RenColor, 35 | font: ^RenFont, 36 | tab_width: i32, 37 | text: [0]u8, 38 | } 39 | 40 | cells_buf1: [CELLS_X * CELLS_Y]u32 41 | cells_buf2: [CELLS_X * CELLS_Y]u32 42 | cells_prev: []u32 = cells_buf1[:] 43 | cells: []u32 = cells_buf2[:] 44 | rect_buf: [CELLS_X * CELLS_Y / 2]RenRect 45 | 46 | command_buf: [COMMAND_BUF_SIZE]u8 47 | command_buf_idx: int 48 | 49 | screen_rect: RenRect 50 | show_debug: bool 51 | 52 | 53 | hash :: proc "contextless" (h: u32, data: []u8) -> u32 { 54 | h := h 55 | for d in data { 56 | h = (h ~ u32(d)) * 16777619 57 | } 58 | return h 59 | } 60 | 61 | cell_idx :: proc "contextless" (x: i32, y: i32) -> i32 { 62 | return x + y * CELLS_X 63 | } 64 | 65 | rects_overlap :: proc "contextless" (a: RenRect, b: RenRect) -> bool { 66 | // odinfmt: disable 67 | return b.x + b.width >= a.x && b.x <= a.x + a.width && 68 | b.y + b.height >= a.y && b.y <= a.y + a.height 69 | // odinfmt: enable 70 | } 71 | 72 | intersect_rects :: proc "contextless" (a: RenRect, b: RenRect) -> RenRect { 73 | x1: i32 = math.max(a.x, b.x) 74 | y1: i32 = math.max(a.y, b.y) 75 | x2: i32 = math.min(a.x + a.width, b.x + b.width) 76 | y2: i32 = math.min(a.y + a.height, b.y + b.height) 77 | return {x1, y1, max(0, x2 - x1), max(0, y2 - y1)} 78 | } 79 | 80 | merge_rects :: proc "contextless" (a: RenRect, b: RenRect) -> RenRect { 81 | x1: i32 = min(a.x, b.x) 82 | y1: i32 = min(a.y, b.y) 83 | x2: i32 = max(a.x + a.width, b.x + b.width) 84 | y2: i32 = max(a.y + a.height, b.y + b.height) 85 | return {x1, y1, x2 - x1, y2 - y1} 86 | } 87 | 88 | push_command :: proc(type: CommandType, size: int) -> ^Command { 89 | 90 | cmd := cast(^Command)&command_buf[command_buf_idx] 91 | n := command_buf_idx + size 92 | if n > COMMAND_BUF_SIZE { 93 | fmt.println("Command buffer exhausted!") 94 | return nil 95 | } 96 | command_buf_idx = n 97 | runtime.memset(cmd, 0, size) 98 | cmd.type = type 99 | cmd.size = cast(i32)size 100 | return cmd 101 | } 102 | 103 | next_command :: proc(prev: ^^Command) -> bool { 104 | if prev^ == nil { 105 | prev^ = cast(^Command)&command_buf[0] 106 | } else { 107 | cmd := prev^ 108 | prev^ = (^Command)(uintptr(cmd) + uintptr(cmd.size)) 109 | } 110 | return prev^ != (cast(^Command)&command_buf[command_buf_idx]) 111 | } 112 | 113 | rencache_show_debug :: proc "contextless" (enable: bool) { 114 | show_debug = enable 115 | } 116 | 117 | rencache_free_font :: proc(font: ^RenFont) { 118 | cmd := push_command(.FREE_FONT, size_of(Command)) 119 | if cmd != nil do cmd.font = font 120 | } 121 | 122 | rencache_set_clip_rect :: proc(rect: RenRect) { 123 | cmd := push_command(.SET_CLIP, size_of(Command)) 124 | if cmd != nil do cmd.rect = intersect_rects(rect, screen_rect) 125 | } 126 | 127 | rencache_draw_rect :: proc(rect: RenRect, color: RenColor) { 128 | if !rects_overlap(screen_rect, rect) do return 129 | cmd := push_command(.DRAW_RECT, size_of(Command)) 130 | if cmd != nil { 131 | cmd.rect = intersect_rects(rect, screen_rect) 132 | cmd.color = color 133 | } 134 | } 135 | 136 | rencache_draw_text :: proc(font: ^RenFont, text: cstring, x: int, y: int, color: RenColor) -> int { 137 | rect: RenRect = --- 138 | rect.x = cast(i32)x 139 | rect.y = cast(i32)y 140 | rect.width = ren_get_font_width(font, text) 141 | rect.height = ren_get_font_height(font) 142 | 143 | if (rects_overlap(screen_rect, rect)) { 144 | text_len := len(text) + 1 145 | cmd := push_command(.DRAW_TEXT, size_of(Command) + text_len) 146 | if cmd != nil { 147 | cmd.color = color 148 | cmd.rect = rect 149 | cmd.font = font 150 | cmd.tab_width = ren_get_font_tab_width(font) 151 | text_buf: [^]u8 = cast([^]u8)&cmd.text 152 | text_in: [^]u8 = cast([^]u8)text 153 | 154 | for i in 0.. int { 196 | /* try to merge with existing rectangle */ 197 | #reverse for &rp in rect_buf[0:count] { 198 | if (rects_overlap(rp, r)) { 199 | rp = merge_rects(rp, r) 200 | return count 201 | } 202 | } 203 | /* couldn't merge with previous rectangle: push */ 204 | rect_buf[count] = r 205 | 206 | count := count 207 | count += 1 208 | return count 209 | } 210 | 211 | rencache_end_frame :: proc() { 212 | /* update cells from commands */ 213 | cr: RenRect 214 | cmd: ^Command 215 | for next_command(&cmd) { 216 | if cmd.type == CommandType.SET_CLIP { 217 | cr = cmd.rect 218 | } 219 | r := intersect_rects(cmd.rect, cr) 220 | if (r.width == 0 || r.height == 0) { 221 | continue 222 | } 223 | h: u32 = HASH_INITIAL 224 | 225 | // hash the bytes 226 | off := cast(i32)(uintptr(cmd) - uintptr(&command_buf[0])) 227 | h = hash(h, command_buf[off:off + cmd.size]) 228 | update_overlapping_cells(r, h) 229 | } 230 | 231 | /* push rects for all cells changed from last frame, reset cells */ 232 | rect_count := 0 233 | max_x := screen_rect.width / CELL_SIZE + 1 234 | max_y := screen_rect.height / CELL_SIZE + 1 235 | for y in 0 ..< max_y { 236 | for x in 0 ..< max_x { 237 | /* compare previous and current cell for change */ 238 | idx := cell_idx(x, y) 239 | if (cells[idx] != cells_prev[idx]) { 240 | rect_count = push_rect(RenRect{x, y, 1, 1}, rect_count) 241 | } 242 | cells_prev[idx] = HASH_INITIAL 243 | } 244 | } 245 | 246 | /* expand rects from cells to pixels */ 247 | for &r in rect_buf[0:rect_count] { 248 | r.x *= CELL_SIZE 249 | r.y *= CELL_SIZE 250 | r.width *= CELL_SIZE 251 | r.height *= CELL_SIZE 252 | r = intersect_rects(r, screen_rect) 253 | } 254 | 255 | /* redraw updated regions */ 256 | has_free_commands := false 257 | for &r in rect_buf[0:rect_count] { 258 | /* draw */ 259 | ren_set_clip_rect(r) 260 | 261 | cmd = nil 262 | for next_command(&cmd) { 263 | switch cmd.type { 264 | case .FREE_FONT: 265 | has_free_commands = true 266 | case .SET_CLIP: 267 | ren_set_clip_rect(intersect_rects(cmd.rect, r)) 268 | case .DRAW_RECT: 269 | ren_draw_rect(cmd.rect, cmd.color) 270 | case .DRAW_TEXT: 271 | ren_set_font_tab_width(cmd.font, cmd.tab_width) 272 | text := strings.string_from_ptr( 273 | cast([^]u8)&cmd.text, 274 | int(cmd.size) - size_of(Command), 275 | ) 276 | ren_draw_text(cmd.font, text, cmd.rect.x, cmd.rect.y, cmd.color) 277 | } 278 | } 279 | if (show_debug) { 280 | color := RenColor{u8(rand.uint32()), u8(rand.uint32()), u8(rand.uint32()), 50} // red(bgra) 281 | ren_draw_rect(r, color) 282 | } 283 | } 284 | 285 | /* update dirty rects */ 286 | if rect_count > 0 { 287 | ren_update_rects(raw_data(&rect_buf), i32(rect_count)) 288 | } 289 | 290 | // /* free fonts */ 291 | if has_free_commands { 292 | cmd = nil 293 | for next_command(&cmd) { 294 | if (cmd.type == CommandType.FREE_FONT) { 295 | ren_free_font(cmd.font) 296 | } 297 | } 298 | } 299 | 300 | // reset command buffer 301 | command_buf_idx = 0 302 | /* swap cell buffer and reset */ 303 | cells, cells_prev = cells_prev, cells 304 | } 305 | 306 | -------------------------------------------------------------------------------- /data/plugins/projectsearch.lua: -------------------------------------------------------------------------------- 1 | local core = require "core" 2 | local common = require "core.common" 3 | local keymap = require "core.keymap" 4 | local command = require "core.command" 5 | local style = require "core.style" 6 | local View = require "core.view" 7 | local translate = require "core.doc.translate" 8 | 9 | 10 | local ResultsView = View:extend() 11 | 12 | 13 | function ResultsView:new(text, fn) 14 | ResultsView.super.new(self) 15 | self.scrollable = true 16 | self.brightness = 0 17 | self:begin_search(text, fn) 18 | end 19 | 20 | 21 | function ResultsView:get_name() 22 | return "Search Results" 23 | end 24 | 25 | 26 | local function find_all_matches_in_file(t, filename, fn) 27 | local fp = io.open(filename) 28 | if not fp then return t end 29 | local n = 1 30 | for line in fp:lines() do 31 | local s = fn(line) 32 | if s then 33 | table.insert(t, { file = filename, text = line, line = n, col = s }) 34 | core.redraw = true 35 | end 36 | if n % 100 == 0 then coroutine.yield() end 37 | n = n + 1 38 | core.redraw = true 39 | end 40 | fp:close() 41 | end 42 | 43 | 44 | function ResultsView:begin_search(text, fn) 45 | self.search_args = { text, fn } 46 | self.results = {} 47 | self.last_file_idx = 1 48 | self.query = text 49 | self.searching = true 50 | self.selected_idx = 0 51 | 52 | core.add_thread(function() 53 | for i, file in ipairs(core.project_files) do 54 | if file.type == "file" then 55 | local result_objects = {system.search_file_find(text, file.filename)} 56 | for _,v in pairs(result_objects) do 57 | table.insert(self.results, { file = v.file, text = v.text, line = v.line, col = v.col }) 58 | end 59 | -- find_all_matches_in_file(self.results, file.filename, fn) 60 | end 61 | self.last_file_idx = i 62 | end 63 | self.searching = false 64 | self.brightness = 100 65 | core.redraw = true 66 | end, self.results) 67 | 68 | self.scroll.to.y = 0 69 | end 70 | 71 | 72 | function ResultsView:refresh() 73 | self:begin_search(table.unpack(self.search_args)) 74 | end 75 | 76 | 77 | function ResultsView:on_mouse_moved(mx, my, ...) 78 | ResultsView.super.on_mouse_moved(self, mx, my, ...) 79 | self.selected_idx = 0 80 | for i, item, x,y,w,h in self:each_visible_result() do 81 | if mx >= x and my >= y and mx < x + w and my < y + h then 82 | self.selected_idx = i 83 | break 84 | end 85 | end 86 | end 87 | 88 | 89 | function ResultsView:on_mouse_pressed(...) 90 | local caught = ResultsView.super.on_mouse_pressed(self, ...) 91 | if not caught then 92 | self:open_selected_result() 93 | end 94 | end 95 | 96 | 97 | function ResultsView:open_selected_result() 98 | local res = self.results[self.selected_idx] 99 | if not res then 100 | return 101 | end 102 | core.try(function() 103 | local dv = core.root_view:open_doc(core.open_doc(res.file)) 104 | core.root_view.root_node:update_layout() 105 | dv.doc:set_selection(res.line, res.col) 106 | dv:scroll_to_line(res.line, false, true) 107 | end) 108 | end 109 | 110 | 111 | function ResultsView:update() 112 | self:move_towards("brightness", 0, 0.1) 113 | ResultsView.super.update(self) 114 | end 115 | 116 | 117 | function ResultsView:get_results_yoffset() 118 | return style.font:get_height() + style.padding.y * 3 119 | end 120 | 121 | 122 | function ResultsView:get_line_height() 123 | return style.padding.y + style.font:get_height() 124 | end 125 | 126 | 127 | function ResultsView:get_scrollable_size() 128 | return self:get_results_yoffset() + #self.results * self:get_line_height() 129 | end 130 | 131 | 132 | function ResultsView:get_visible_results_range() 133 | local lh = self:get_line_height() 134 | local oy = self:get_results_yoffset() 135 | local min = math.max(1, math.floor((self.scroll.y - oy) / lh)) 136 | return min, min + math.floor(self.size.y / lh) + 1 137 | end 138 | 139 | 140 | function ResultsView:each_visible_result() 141 | return coroutine.wrap(function() 142 | local lh = self:get_line_height() 143 | local x, y = self:get_content_offset() 144 | local min, max = self:get_visible_results_range() 145 | y = y + self:get_results_yoffset() + lh * (min - 1) 146 | for i = min, max do 147 | local item = self.results[i] 148 | if not item then break end 149 | coroutine.yield(i, item, x, y, self.size.x, lh) 150 | y = y + lh 151 | end 152 | end) 153 | end 154 | 155 | 156 | function ResultsView:scroll_to_make_selected_visible() 157 | local h = self:get_line_height() 158 | local y = self:get_results_yoffset() + h * (self.selected_idx - 1) 159 | self.scroll.to.y = math.min(self.scroll.to.y, y) 160 | self.scroll.to.y = math.max(self.scroll.to.y, y + h - self.size.y) 161 | end 162 | 163 | 164 | function ResultsView:draw() 165 | self:draw_background(style.background) 166 | 167 | -- status 168 | local ox, oy = self:get_content_offset() 169 | local x, y = ox + style.padding.x, oy + style.padding.y 170 | local per = self.last_file_idx / #core.project_files 171 | local text 172 | if self.searching then 173 | text = string.format("Searching %d%% (%d of %d files, %d matches) for %q...", 174 | math.tointeger(math.floor(per * 100)), self.last_file_idx, #core.project_files, 175 | #self.results, self.query) 176 | else 177 | text = string.format("Found %d matches for %q", 178 | #self.results, self.query) 179 | end 180 | local color = common.lerp(style.text, style.accent, self.brightness / 100) 181 | renderer.draw_text(style.font, text, x, y, color) 182 | 183 | -- horizontal line 184 | local yoffset = self:get_results_yoffset() 185 | local x = ox + style.padding.x 186 | local w = self.size.x - style.padding.x * 2 187 | local h = style.divider_size 188 | local color = common.lerp(style.dim, style.text, self.brightness / 100) 189 | renderer.draw_rect(x, oy + yoffset - style.padding.y, w, h, color) 190 | if self.searching then 191 | renderer.draw_rect(x, oy + yoffset - style.padding.y, w * per, h, style.text) 192 | end 193 | 194 | -- results 195 | local y1, y2 = self.position.y, self.position.y + self.size.y 196 | for i, item, x,y,w,h in self:each_visible_result() do 197 | local color = style.text 198 | if i == self.selected_idx then 199 | color = style.accent 200 | renderer.draw_rect(x, y, w, h, style.line_highlight) 201 | end 202 | x = x + style.padding.x 203 | local text = string.format("%s at line %d (col %d): ", item.file, item.line, item.col) 204 | x = common.draw_text(style.font, style.dim, text, "left", x, y, w, h) 205 | x = common.draw_text(style.code_font, color, item.text, "left", x, y, w, h) 206 | end 207 | 208 | self:draw_scrollbar() 209 | end 210 | 211 | 212 | local function begin_search(text, fn) 213 | if text == "" then 214 | core.error("Expected non-empty string") 215 | return 216 | end 217 | local rv = ResultsView(text, fn) 218 | core.root_view:get_active_node():add_view(rv) 219 | end 220 | 221 | 222 | command.add(nil, { 223 | ["project-search:find"] = function() 224 | -- try to get the word under cursor or use selection 225 | local doc = core.active_view.doc 226 | if doc then 227 | local line1, col1, line2, col2 = doc:get_selection(true) 228 | if doc:has_selection() == false then 229 | line1, col1 = translate.start_of_word(doc, line1, col1) 230 | line2, col2 = translate.end_of_word(doc, line1, col1) 231 | end 232 | local text = doc:get_text(line1, col1, line2, col2) 233 | core.command_view:set_text(text, nil) 234 | end 235 | 236 | core.command_view:enter("Find Text In Project", function(text) 237 | text = text:lower() 238 | begin_search(text, function(line_text) 239 | return line_text:lower():find(text, nil, true) 240 | end) 241 | end) 242 | end, 243 | 244 | ["project-search:find-pattern"] = function() 245 | core.command_view:enter("Find Pattern In Project", function(text) 246 | begin_search(text, function(line_text) return line_text:find(text) end) 247 | end) 248 | end, 249 | 250 | ["project-search:fuzzy-find"] = function() 251 | core.command_view:enter("Fuzzy Find Text In Project", function(text) 252 | begin_search(text, function(line_text) 253 | return common.fuzzy_match(line_text, text) and 1 254 | end) 255 | end) 256 | end, 257 | }) 258 | 259 | 260 | command.add(ResultsView, { 261 | ["project-search:select-previous"] = function() 262 | local view = core.active_view 263 | view.selected_idx = math.max(view.selected_idx - 1, 1) 264 | view:scroll_to_make_selected_visible() 265 | end, 266 | 267 | ["project-search:select-next"] = function() 268 | local view = core.active_view 269 | view.selected_idx = math.min(view.selected_idx + 1, #view.results) 270 | view:scroll_to_make_selected_visible() 271 | end, 272 | 273 | ["project-search:open-selected"] = function() 274 | core.active_view:open_selected_result() 275 | end, 276 | 277 | ["project-search:refresh"] = function() 278 | core.active_view:refresh() 279 | end, 280 | }) 281 | 282 | keymap.add { 283 | ["f5"] = "project-search:refresh", 284 | ["ctrl+shift+f"] = "project-search:find", 285 | ["up"] = "project-search:select-previous", 286 | ["down"] = "project-search:select-next", 287 | ["return"] = "project-search:open-selected", 288 | } 289 | -------------------------------------------------------------------------------- /renderer.odin: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "base:runtime" 4 | import "core:fmt" 5 | import "core:math" 6 | import "core:os" 7 | 8 | import sdl "vendor:sdl2" 9 | import stbtt "vendor:stb/truetype" 10 | 11 | @(private) 12 | clip: ClipRect 13 | 14 | @(private) 15 | MAX_GLYPHSET :: 256 16 | 17 | RenColor :: struct { 18 | b, g, r, a: u8, 19 | } 20 | 21 | RenRect :: struct { 22 | x, y, width, height: i32, 23 | } 24 | 25 | RenImage :: struct { 26 | pixels: []RenColor, 27 | width, height: i32, 28 | } 29 | 30 | ClipRect :: struct { 31 | left, top, right, bottom: i32, 32 | } 33 | 34 | GlyphSet :: struct { 35 | image: ^RenImage, 36 | glyphs: [256]stbtt.bakedchar, 37 | } 38 | 39 | RenFont :: struct { 40 | data: []byte, 41 | stbfont: stbtt.fontinfo, 42 | sets: [MAX_GLYPHSET]^GlyphSet, 43 | size: f32, 44 | height: i32, 45 | } 46 | 47 | initial_frame: bool 48 | loaded_fonts: [dynamic]^RenFont 49 | default_allocator: runtime.Allocator 50 | 51 | ren_init :: proc(win: ^sdl.Window) { 52 | window = win 53 | surf: ^sdl.Surface = sdl.GetWindowSurface(window) 54 | ren_set_clip_rect(RenRect{0, 0, surf.w, surf.h}) 55 | default_allocator = context.allocator 56 | } 57 | 58 | ren_update_rects :: proc "contextless" (rects: [^]RenRect, count: i32) { 59 | sdl.UpdateWindowSurfaceRects(window, cast([^]sdl.Rect)rects, count) 60 | initial_frame = true 61 | if initial_frame { 62 | sdl.ShowWindow(window) 63 | initial_frame = false 64 | } 65 | } 66 | 67 | ren_set_clip_rect :: proc "contextless" (rect: RenRect) { 68 | clip.left = rect.x 69 | clip.top = rect.y 70 | clip.right = rect.x + rect.width 71 | clip.bottom = rect.y + rect.height 72 | } 73 | 74 | ren_get_size :: proc "contextless" (x: ^i32, y: ^i32) { 75 | surf: ^sdl.Surface = sdl.GetWindowSurface(window) 76 | x^ = surf.w 77 | y^ = surf.h 78 | } 79 | 80 | ren_new_image :: proc(width: i32, height: i32) -> ^RenImage { 81 | assert(width > 0 && height > 0) 82 | image: ^RenImage = new(RenImage, default_allocator) 83 | image.pixels = make([]RenColor, width * height, default_allocator) 84 | image.width = width 85 | image.height = height 86 | return image 87 | } 88 | 89 | ren_free_image :: proc(image: ^RenImage) { 90 | delete(image.pixels, default_allocator) 91 | free(image, default_allocator) 92 | } 93 | 94 | load_glyphset :: proc(font: ^RenFont, idx: i32) -> ^GlyphSet { 95 | set := new(GlyphSet, default_allocator) 96 | /* init image */ 97 | width: i32 = 128 98 | height: i32 = 128 99 | 100 | 101 | done: i32 = -1 102 | set.image = ren_new_image(width, height) 103 | for done < 0 { 104 | // /* load glyphs */ 105 | s := 106 | stbtt.ScaleForMappingEmToPixels(&font.stbfont, 1) / 107 | stbtt.ScaleForPixelHeight(&font.stbfont, 1) 108 | 109 | res: i32 = stbtt.BakeFontBitmap( 110 | raw_data(font.data), 111 | 0, 112 | font.size * s, 113 | cast([^]u8)raw_data(set.image.pixels), 114 | width, 115 | height, 116 | idx * 256, 117 | 256, 118 | raw_data(&set.glyphs), 119 | ) 120 | 121 | /* retry with a larger image buffer if the buffer wasn't large enough */ 122 | if (res < 0) { 123 | width *= 2 124 | height *= 2 125 | ren_free_image(set.image) 126 | set.image = ren_new_image(width, height) 127 | } 128 | done = res 129 | } 130 | /* adjust glyph yoffsets and xadvance */ 131 | ascent, descent, linegap: i32 132 | stbtt.GetFontVMetrics(&font.stbfont, &ascent, &descent, &linegap) 133 | scale: f32 = stbtt.ScaleForMappingEmToPixels(&font.stbfont, font.size) 134 | scaled_ascent: i32 = cast(i32)(f32(ascent) * scale + 0.5) 135 | 136 | for i in 0 ..< 256 { 137 | set.glyphs[i].yoff += f32(scaled_ascent) 138 | set.glyphs[i].xadvance = math.floor(set.glyphs[i].xadvance) 139 | } 140 | 141 | /* convert 8bit data to 32bit */ 142 | for i := width * height - 1; i >= 0; i -= 1 { 143 | raw_pixels: [^]RenColor = raw_data(set.image.pixels) 144 | n: u8 = (cast([^]u8)raw_pixels)[i] 145 | set.image.pixels[i] = RenColor { 146 | r = 255, 147 | g = 255, 148 | b = 255, 149 | a = n, 150 | } 151 | } 152 | return set 153 | } 154 | 155 | get_glyphset :: proc(font: ^RenFont, codepoint: i32) -> ^GlyphSet { 156 | idx := (codepoint >> 8) % MAX_GLYPHSET 157 | assert(font != nil) 158 | if font.sets[idx] == nil { 159 | font.sets[idx] = load_glyphset(font, idx) 160 | } 161 | return font.sets[idx] 162 | } 163 | 164 | ren_load_font :: proc(filename: cstring, size: f32) -> ^RenFont { 165 | /* init font */ 166 | font := new(RenFont, default_allocator) 167 | font.size = size 168 | 169 | /* load font into buffer */ 170 | data, success := os.read_entire_file_from_filename(string(filename), default_allocator) 171 | if !success { 172 | fmt.println("Failed to read file from filename", filename) 173 | free(font, default_allocator) 174 | return nil 175 | } 176 | font.data = data 177 | 178 | /* init stbfont */ 179 | ok := cast(i32)stbtt.InitFont(&font.stbfont, raw_data(font.data), 0) 180 | if ok == 0 { 181 | fmt.println("Failed to init font") 182 | return nil 183 | } 184 | 185 | /* get height and scale */ 186 | ascent, descent, linegap: i32 187 | stbtt.GetFontVMetrics(&font.stbfont, &ascent, &descent, &linegap) 188 | scale := stbtt.ScaleForMappingEmToPixels(&font.stbfont, size) 189 | font.height = cast(i32)(cast(f32)(ascent - descent + linegap) * scale + 0.5) 190 | 191 | /* make tab and newline glyphs invisible */ 192 | set: ^GlyphSet = get_glyphset(font, '\n') 193 | set.glyphs['\t'].x1 = set.glyphs['\t'].x0 194 | set.glyphs['\n'].x1 = set.glyphs['\n'].x0 195 | append(&loaded_fonts, font) 196 | 197 | return font 198 | } 199 | 200 | ren_free_font :: proc(font: ^RenFont) { 201 | for f, i in loaded_fonts { 202 | if f == font { 203 | unordered_remove(&loaded_fonts, i) 204 | break 205 | } 206 | } 207 | 208 | for i in 0 ..< MAX_GLYPHSET { 209 | set: ^GlyphSet = font.sets[i] 210 | if set != nil { 211 | ren_free_image(set.image) 212 | free(set, default_allocator) 213 | } 214 | } 215 | delete(font.data, default_allocator) 216 | free(font, default_allocator) 217 | } 218 | 219 | ren_free_fonts :: proc() { 220 | size := len(loaded_fonts) 221 | for i := 0; i < size; i += 1 { 222 | ren_free_font(pop(&loaded_fonts)) 223 | } 224 | assert(len(loaded_fonts) == 0) 225 | } 226 | 227 | ren_set_font_tab_width :: proc(font: ^RenFont, n: i32) { 228 | set: ^GlyphSet = get_glyphset(font, '\t') 229 | set.glyphs['\t'].xadvance = cast(f32)n 230 | } 231 | 232 | ren_get_font_tab_width :: proc(font: ^RenFont) -> i32 { 233 | set: ^GlyphSet = get_glyphset(font, '\t') 234 | return cast(i32)set.glyphs['\t'].xadvance 235 | } 236 | 237 | ren_get_font_width :: proc(font: ^RenFont, text: cstring) -> i32 { 238 | x: i32 = 0 239 | p := string(text) // not a copy 240 | for codepoint in p { 241 | set: ^GlyphSet = get_glyphset(font, cast(i32)codepoint) 242 | g: ^stbtt.bakedchar = &set.glyphs[codepoint & 0xff] 243 | x += cast(i32)g.xadvance 244 | } 245 | return x 246 | } 247 | 248 | ren_get_font_height :: proc "contextless" (font: ^RenFont) -> i32 { 249 | return font.height 250 | } 251 | 252 | blend_pixel :: #force_inline proc "contextless" (dst: RenColor, src: RenColor) -> RenColor { 253 | dst := dst 254 | 255 | ia := u32(0xff - src.a) 256 | src_a := u32(src.a) 257 | dst.r = u8(((u32(src.r) * src_a) + (u32(dst.r) * ia)) >> 8) 258 | dst.g = u8(((u32(src.g) * src_a) + (u32(dst.g) * ia)) >> 8) 259 | dst.b = u8(((u32(src.b) * src_a) + (u32(dst.b) * ia)) >> 8) 260 | return dst 261 | } 262 | 263 | blend_pixel2 :: #force_inline proc "contextless" ( 264 | dst: RenColor, 265 | src: RenColor, 266 | color: RenColor, 267 | ) -> RenColor { 268 | dst := dst 269 | 270 | src_a := (u32(src.a) * u32(color.a)) >> 8 271 | ia: u32 = u32(0xff - src.a) 272 | dst.r = u8((u32(src.r) * u32(color.r) * src_a >> 16) + ((u32(dst.r) * ia) >> 8)) 273 | dst.g = u8((u32(src.g) * u32(color.g) * src_a >> 16) + ((u32(dst.g) * ia) >> 8)) 274 | dst.b = u8((u32(src.b) * u32(color.b) * src_a >> 16) + ((u32(dst.b) * ia) >> 8)) 275 | 276 | return dst 277 | } 278 | 279 | ren_draw_rect :: proc "contextless" (rect: RenRect, color: RenColor) { 280 | if (color.a == 0) { 281 | return 282 | } 283 | 284 | x1: i32 = max(rect.x, clip.left) 285 | y1: i32 = max(rect.y, clip.top) 286 | x2: i32 = min(rect.x + rect.width, clip.right) 287 | y2: i32 = min(rect.y + rect.height, clip.bottom) 288 | rect_width := x2 - x1 289 | 290 | surf: ^sdl.Surface = sdl.GetWindowSurface(window) 291 | d := cast([^]RenColor)surf.pixels 292 | row_start := x1 + y1 * surf.w 293 | 294 | if color.a == 0xff { 295 | for _ in y1 ..< y2 { 296 | for i in 0 ..< rect_width { 297 | d[row_start + i] = color 298 | } 299 | row_start += surf.w 300 | } 301 | } else { 302 | for _ in y1 ..< y2 { 303 | for i in 0 ..< rect_width { 304 | d[row_start + i] = blend_pixel(d[i], color) 305 | } 306 | row_start += surf.w 307 | } 308 | } 309 | } 310 | 311 | ren_draw_image :: proc "contextless" ( 312 | image: ^RenImage, 313 | sub: ^RenRect, 314 | x: i32, 315 | y: i32, 316 | color: RenColor, 317 | ) { 318 | if color.a == 0 { 319 | return 320 | } 321 | x := x 322 | y := y 323 | 324 | /* clip */ 325 | n := clip.left - x 326 | if n > 0 { 327 | sub.width -= n 328 | sub.x += n 329 | x += n 330 | } 331 | 332 | n = clip.top - y 333 | if n > 0 { 334 | sub.height -= n 335 | sub.y += n 336 | y += n 337 | } 338 | 339 | n = x + sub.width - clip.right 340 | if n > 0 { 341 | sub.width -= n 342 | } 343 | 344 | n = y + sub.height - clip.bottom 345 | if n > 0 { 346 | sub.height -= n 347 | } 348 | 349 | if (sub.width <= 0 || sub.height <= 0) { 350 | return 351 | } 352 | 353 | /* draw */ 354 | surf: ^sdl.Surface = sdl.GetWindowSurface(window) 355 | s: [^]RenColor = raw_data(image.pixels) 356 | d: [^]RenColor = cast([^]RenColor)(surf.pixels) 357 | image_row := sub.x + sub.y * image.width 358 | surf_row := x + y * surf.w 359 | 360 | for _ in 0 ..< sub.height { 361 | for i in 0 ..< sub.width { 362 | d[surf_row + i] = blend_pixel2(d[surf_row + i], s[image_row + i], color) 363 | } 364 | image_row += image.width 365 | surf_row += surf.w 366 | } 367 | } 368 | 369 | ren_draw_text :: proc(font: ^RenFont, text: string, x: i32, y: i32, color: RenColor) -> i32 { 370 | rect: RenRect 371 | x := x 372 | 373 | for codepoint in text { 374 | set: ^GlyphSet = get_glyphset(font, cast(i32)codepoint) 375 | g: ^stbtt.bakedchar = &set.glyphs[codepoint & 0xff] 376 | 377 | rect.x = cast(i32)g.x0 378 | rect.y = cast(i32)g.y0 379 | rect.width = cast(i32)(g.x1 - g.x0) 380 | rect.height = cast(i32)(g.y1 - g.y0) 381 | ren_draw_image( 382 | set.image, 383 | &rect, 384 | cast(i32)(cast(f32)x + g.xoff), 385 | cast(i32)(cast(f32)y + g.yoff), 386 | color, 387 | ) 388 | x += cast(i32)g.xadvance 389 | } 390 | return x 391 | } 392 | 393 | -------------------------------------------------------------------------------- /data/core/commands/doc.lua: -------------------------------------------------------------------------------- 1 | local core = require "core" 2 | local command = require "core.command" 3 | local common = require "core.common" 4 | local config = require "core.config" 5 | local translate = require "core.doc.translate" 6 | local DocView = require "core.docview" 7 | 8 | 9 | local function dv() 10 | return core.active_view 11 | end 12 | 13 | 14 | local function doc() 15 | return core.active_view.doc 16 | end 17 | 18 | 19 | local function get_indent_string() 20 | if config.tab_type == "hard" then 21 | return "\t" 22 | end 23 | return string.rep(" ", config.indent_size) 24 | end 25 | 26 | 27 | local function insert_at_start_of_selected_lines(text, skip_empty) 28 | local line1, col1, line2, col2, swap = doc():get_selection(true) 29 | for line = line1, line2 do 30 | local line_text = doc().lines[line] 31 | if (not skip_empty or line_text:find("%S")) then 32 | doc():insert(line, 1, text) 33 | end 34 | end 35 | doc():set_selection(line1, col1 + #text, line2, col2 + #text, swap) 36 | end 37 | 38 | 39 | local function remove_from_start_of_selected_lines(text, skip_empty) 40 | local line1, col1, line2, col2, swap = doc():get_selection(true) 41 | for line = line1, line2 do 42 | local line_text = doc().lines[line] 43 | if line_text:sub(1, #text) == text 44 | and (not skip_empty or line_text:find("%S")) 45 | then 46 | doc():remove(line, 1, line, #text + 1) 47 | end 48 | end 49 | doc():set_selection(line1, col1 - #text, line2, col2 - #text, swap) 50 | end 51 | 52 | 53 | local function append_line_if_last_line(line) 54 | if line >= #doc().lines then 55 | doc():insert(line, math.huge, "\n") 56 | end 57 | end 58 | 59 | 60 | local function save(filename) 61 | doc():save(filename) 62 | core.log("Saved \"%s\"", doc().filename) 63 | end 64 | 65 | 66 | local commands = { 67 | ["doc:undo"] = function() 68 | doc():undo() 69 | end, 70 | 71 | ["doc:redo"] = function() 72 | doc():redo() 73 | end, 74 | 75 | ["doc:cut"] = function() 76 | if doc():has_selection() then 77 | local text = doc():get_text(doc():get_selection()) 78 | system.set_clipboard(text) 79 | doc():delete_to(0) 80 | end 81 | end, 82 | 83 | ["doc:copy"] = function() 84 | if doc():has_selection() then 85 | local text = doc():get_text(doc():get_selection()) 86 | system.set_clipboard(text) 87 | end 88 | end, 89 | 90 | ["doc:paste"] = function() 91 | doc():text_input(system.get_clipboard():gsub("\r", "")) 92 | end, 93 | 94 | ["doc:newline"] = function() 95 | local line, col = doc():get_selection() 96 | local indent = doc().lines[line]:match("^[\t ]*") 97 | if col <= #indent then 98 | indent = indent:sub(#indent + 2 - col) 99 | end 100 | doc():text_input("\n" .. indent) 101 | end, 102 | 103 | ["doc:newline-below"] = function() 104 | local line = doc():get_selection() 105 | local indent = doc().lines[line]:match("^[\t ]*") 106 | doc():insert(line, math.huge, "\n" .. indent) 107 | doc():set_selection(line + 1, math.huge) 108 | end, 109 | 110 | ["doc:newline-above"] = function() 111 | local line = doc():get_selection() 112 | local indent = doc().lines[line]:match("^[\t ]*") 113 | doc():insert(line, 1, indent .. "\n") 114 | doc():set_selection(line, math.huge) 115 | end, 116 | 117 | ["doc:delete"] = function() 118 | local line, col = doc():get_selection() 119 | if not doc():has_selection() and doc().lines[line]:find("^%s*$", col) then 120 | doc():remove(line, col, line, math.huge) 121 | end 122 | doc():delete_to(translate.next_char) 123 | end, 124 | 125 | ["doc:backspace"] = function() 126 | local line, col = doc():get_selection() 127 | if not doc():has_selection() then 128 | local text = doc():get_text(line, 1, line, col) 129 | if #text >= config.indent_size and text:find("^ *$") then 130 | doc():delete_to(0, -config.indent_size) 131 | return 132 | end 133 | end 134 | doc():delete_to(translate.previous_char) 135 | end, 136 | 137 | ["doc:select-all"] = function() 138 | doc():set_selection(1, 1, math.huge, math.huge) 139 | end, 140 | 141 | ["doc:select-none"] = function() 142 | local line, col = doc():get_selection() 143 | doc():set_selection(line, col) 144 | end, 145 | 146 | ["doc:select-lines"] = function() 147 | local line1, _, line2, _, swap = doc():get_selection(true) 148 | append_line_if_last_line(line2) 149 | doc():set_selection(line1, 1, line2 + 1, 1, swap) 150 | end, 151 | 152 | ["doc:select-word"] = function() 153 | local line1, col1 = doc():get_selection(true) 154 | local line1, col1 = translate.start_of_word(doc(), line1, col1) 155 | local line2, col2 = translate.end_of_word(doc(), line1, col1) 156 | doc():set_selection(line2, col2, line1, col1) 157 | end, 158 | 159 | ["doc:join-lines"] = function() 160 | local line1, _, line2 = doc():get_selection(true) 161 | if line1 == line2 then line2 = line2 + 1 end 162 | local text = doc():get_text(line1, 1, line2, math.huge) 163 | text = text:gsub("(.-)\n[\t ]*", function(x) 164 | return x:find("^%s*$") and x or x .. " " 165 | end) 166 | doc():insert(line1, 1, text) 167 | doc():remove(line1, #text + 1, line2, math.huge) 168 | if doc():has_selection() then 169 | doc():set_selection(line1, math.huge) 170 | end 171 | end, 172 | 173 | ["doc:indent"] = function() 174 | local text = get_indent_string() 175 | if doc():has_selection() then 176 | insert_at_start_of_selected_lines(text) 177 | else 178 | doc():text_input(text) 179 | end 180 | end, 181 | 182 | ["doc:unindent"] = function() 183 | local text = get_indent_string() 184 | remove_from_start_of_selected_lines(text) 185 | end, 186 | 187 | ["doc:duplicate-lines"] = function() 188 | local line1, col1, line2, col2, swap = doc():get_selection(true) 189 | append_line_if_last_line(line2) 190 | local text = doc():get_text(line1, 1, line2 + 1, 1) 191 | doc():insert(line2 + 1, 1, text) 192 | local n = line2 - line1 + 1 193 | doc():set_selection(line1 + n, col1, line2 + n, col2, swap) 194 | end, 195 | 196 | ["doc:delete-lines"] = function() 197 | local line1, col1, line2 = doc():get_selection(true) 198 | append_line_if_last_line(line2) 199 | doc():remove(line1, 1, line2 + 1, 1) 200 | doc():set_selection(line1, col1) 201 | end, 202 | 203 | ["doc:move-lines-up"] = function() 204 | local line1, col1, line2, col2, swap = doc():get_selection(true) 205 | append_line_if_last_line(line2) 206 | if line1 > 1 then 207 | local text = doc().lines[line1 - 1] 208 | doc():insert(line2 + 1, 1, text) 209 | doc():remove(line1 - 1, 1, line1, 1) 210 | doc():set_selection(line1 - 1, col1, line2 - 1, col2, swap) 211 | end 212 | end, 213 | 214 | ["doc:move-lines-down"] = function() 215 | local line1, col1, line2, col2, swap = doc():get_selection(true) 216 | append_line_if_last_line(line2 + 1) 217 | if line2 < #doc().lines then 218 | local text = doc().lines[line2 + 1] 219 | doc():remove(line2 + 1, 1, line2 + 2, 1) 220 | doc():insert(line1, 1, text) 221 | doc():set_selection(line1 + 1, col1, line2 + 1, col2, swap) 222 | end 223 | end, 224 | 225 | ["doc:toggle-line-comments"] = function() 226 | local comment = doc().syntax.comment 227 | if not comment then return end 228 | local comment_text = comment .. " " 229 | local line1, _, line2 = doc():get_selection(true) 230 | local uncomment = true 231 | for line = line1, line2 do 232 | local text = doc().lines[line] 233 | if text:find("%S") and text:find(comment_text, 1, true) ~= 1 then 234 | uncomment = false 235 | end 236 | end 237 | if uncomment then 238 | remove_from_start_of_selected_lines(comment_text, true) 239 | else 240 | insert_at_start_of_selected_lines(comment_text, true) 241 | end 242 | end, 243 | 244 | ["doc:upper-case"] = function() 245 | doc():replace(string.upper) 246 | end, 247 | 248 | ["doc:lower-case"] = function() 249 | doc():replace(string.lower) 250 | end, 251 | 252 | ["doc:go-to-line"] = function() 253 | local dv = dv() 254 | 255 | local items 256 | local function init_items() 257 | if items then return end 258 | items = {} 259 | local mt = { __tostring = function(x) return x.text end } 260 | for i, line in ipairs(dv.doc.lines) do 261 | local item = { text = line:sub(1, -2), line = i, info = "line: " .. i } 262 | table.insert(items, setmetatable(item, mt)) 263 | end 264 | end 265 | 266 | core.command_view:enter("Go To Line", function(text, item) 267 | local line = item and item.line or tonumber(text) 268 | if not line then 269 | core.error("Invalid line number or unmatched string") 270 | return 271 | end 272 | dv.doc:set_selection(line, 1 ) 273 | dv:scroll_to_line(line, true) 274 | 275 | end, function(text) 276 | if not text:find("^%d*$") then 277 | init_items() 278 | return common.fuzzy_match(items, text) 279 | end 280 | end) 281 | end, 282 | 283 | ["doc:toggle-line-ending"] = function() 284 | doc().crlf = not doc().crlf 285 | end, 286 | 287 | ["doc:save-as"] = function() 288 | if doc().filename then 289 | core.command_view:set_text(doc().filename) 290 | end 291 | core.command_view:enter("Save As", function(filename) 292 | save(filename) 293 | end, common.path_suggest) 294 | end, 295 | 296 | ["doc:save"] = function() 297 | if doc().filename then 298 | save() 299 | else 300 | command.perform("doc:save-as") 301 | end 302 | end, 303 | 304 | ["doc:rename"] = function() 305 | local old_filename = doc().filename 306 | if not old_filename then 307 | core.error("Cannot rename unsaved doc") 308 | return 309 | end 310 | core.command_view:set_text(old_filename) 311 | core.command_view:enter("Rename", function(filename) 312 | doc():save(filename) 313 | core.log("Renamed \"%s\" to \"%s\"", old_filename, filename) 314 | if filename ~= old_filename then 315 | os.remove(old_filename) 316 | end 317 | end, common.path_suggest) 318 | end, 319 | } 320 | 321 | 322 | local translations = { 323 | ["previous-char"] = translate.previous_char, 324 | ["next-char"] = translate.next_char, 325 | ["previous-word-start"] = translate.previous_word_start, 326 | ["next-word-end"] = translate.next_word_end, 327 | ["previous-block-start"] = translate.previous_block_start, 328 | ["next-block-end"] = translate.next_block_end, 329 | ["start-of-doc"] = translate.start_of_doc, 330 | ["end-of-doc"] = translate.end_of_doc, 331 | ["start-of-line"] = translate.start_of_line, 332 | ["end-of-line"] = translate.end_of_line, 333 | ["start-of-word"] = translate.start_of_word, 334 | ["end-of-word"] = translate.end_of_word, 335 | ["previous-line"] = DocView.translate.previous_line, 336 | ["next-line"] = DocView.translate.next_line, 337 | ["previous-page"] = DocView.translate.previous_page, 338 | ["next-page"] = DocView.translate.next_page, 339 | } 340 | 341 | for name, fn in pairs(translations) do 342 | commands["doc:move-to-" .. name] = function() doc():move_to(fn, dv()) end 343 | commands["doc:select-to-" .. name] = function() doc():select_to(fn, dv()) end 344 | commands["doc:delete-to-" .. name] = function() doc():delete_to(fn, dv()) end 345 | end 346 | 347 | commands["doc:move-to-previous-char"] = function() 348 | if doc():has_selection() then 349 | local line, col = doc():get_selection(true) 350 | doc():set_selection(line, col) 351 | else 352 | doc():move_to(translate.previous_char) 353 | end 354 | end 355 | 356 | commands["doc:move-to-next-char"] = function() 357 | if doc():has_selection() then 358 | local _, _, line, col = doc():get_selection(true) 359 | doc():set_selection(line, col) 360 | else 361 | doc():move_to(translate.next_char) 362 | end 363 | end 364 | 365 | command.add("core.docview", commands) 366 | -------------------------------------------------------------------------------- /data/core/doc/init.lua: -------------------------------------------------------------------------------- 1 | local Object = require "core.object" 2 | local Highlighter = require "core.doc.highlighter" 3 | local syntax = require "core.syntax" 4 | local config = require "core.config" 5 | local common = require "core.common" 6 | 7 | 8 | local Doc = Object:extend() 9 | 10 | 11 | local function split_lines(text) 12 | local res = {} 13 | for line in (text .. "\n"):gmatch("(.-)\n") do 14 | table.insert(res, line) 15 | end 16 | return res 17 | end 18 | 19 | 20 | local function splice(t, at, remove, insert) 21 | insert = insert or {} 22 | local offset = #insert - remove 23 | local old_len = #t 24 | if offset < 0 then 25 | for i = at - offset, old_len - offset do 26 | t[i + offset] = t[i] 27 | end 28 | elseif offset > 0 then 29 | for i = old_len, at, -1 do 30 | t[i + offset] = t[i] 31 | end 32 | end 33 | for i, item in ipairs(insert) do 34 | t[at + i - 1] = item 35 | end 36 | end 37 | 38 | 39 | function Doc:new(filename) 40 | self:reset() 41 | if filename then 42 | self:load(filename) 43 | end 44 | end 45 | 46 | 47 | function Doc:reset() 48 | self.lines = { "\n" } 49 | self.selection = { a = { line=1, col=1 }, b = { line=1, col=1 } } 50 | self.undo_stack = { idx = 1 } 51 | self.redo_stack = { idx = 1 } 52 | self.clean_change_id = 1 53 | self.highlighter = Highlighter(self) 54 | self:reset_syntax() 55 | end 56 | 57 | 58 | function Doc:reset_syntax() 59 | local header = self:get_text(1, 1, self:position_offset(1, 1, 128)) 60 | local syn = syntax.get(self.filename or "", header) 61 | if self.syntax ~= syn then 62 | self.syntax = syn 63 | self.highlighter:reset() 64 | end 65 | end 66 | 67 | 68 | function Doc:load(filename) 69 | local fp = assert( io.open(filename, "rb") ) 70 | self:reset() 71 | self.filename = filename 72 | self.lines = {} 73 | for line in fp:lines() do 74 | if line:byte(-1) == 13 then 75 | line = line:sub(1, -2) 76 | self.crlf = true 77 | end 78 | table.insert(self.lines, line .. "\n") 79 | end 80 | if #self.lines == 0 then 81 | table.insert(self.lines, "\n") 82 | end 83 | fp:close() 84 | self:reset_syntax() 85 | end 86 | 87 | 88 | function Doc:save(filename) 89 | filename = filename or assert(self.filename, "no filename set to default to") 90 | local fp = assert( io.open(filename, "wb") ) 91 | for _, line in ipairs(self.lines) do 92 | if self.crlf then line = line:gsub("\n", "\r\n") end 93 | fp:write(line) 94 | end 95 | fp:close() 96 | self.filename = filename or self.filename 97 | self:reset_syntax() 98 | self:clean() 99 | end 100 | 101 | 102 | function Doc:get_name() 103 | return self.filename or "unsaved" 104 | end 105 | 106 | 107 | function Doc:is_dirty() 108 | return self.clean_change_id ~= self:get_change_id() 109 | end 110 | 111 | 112 | function Doc:clean() 113 | self.clean_change_id = self:get_change_id() 114 | end 115 | 116 | 117 | function Doc:get_change_id() 118 | return self.undo_stack.idx 119 | end 120 | 121 | 122 | function Doc:set_selection(line1, col1, line2, col2, swap) 123 | assert(not line2 == not col2, "expected 2 or 4 arguments") 124 | if swap then line1, col1, line2, col2 = line2, col2, line1, col1 end 125 | line1, col1 = self:sanitize_position(line1, col1) 126 | line2, col2 = self:sanitize_position(line2 or line1, col2 or col1) 127 | self.selection.a.line, self.selection.a.col = line1, col1 128 | self.selection.b.line, self.selection.b.col = line2, col2 129 | end 130 | 131 | 132 | local function sort_positions(line1, col1, line2, col2) 133 | if line1 > line2 134 | or line1 == line2 and col1 > col2 then 135 | return line2, col2, line1, col1, true 136 | end 137 | return line1, col1, line2, col2, false 138 | end 139 | 140 | 141 | function Doc:get_selection(sort) 142 | local a, b = self.selection.a, self.selection.b 143 | if sort then 144 | return sort_positions(a.line, a.col, b.line, b.col) 145 | end 146 | return a.line, a.col, b.line, b.col 147 | end 148 | 149 | 150 | function Doc:has_selection() 151 | local a, b = self.selection.a, self.selection.b 152 | return not (a.line == b.line and a.col == b.col) 153 | end 154 | 155 | 156 | function Doc:sanitize_selection() 157 | self:set_selection(self:get_selection()) 158 | end 159 | 160 | 161 | function Doc:sanitize_position(line, col) 162 | line = common.clamp(line, 1, #self.lines) 163 | col = common.clamp(col, 1, #self.lines[line]) 164 | return line, col 165 | end 166 | 167 | 168 | local function position_offset_func(self, line, col, fn, ...) 169 | line, col = self:sanitize_position(line, col) 170 | return fn(self, line, col, ...) 171 | end 172 | 173 | 174 | local function position_offset_byte(self, line, col, offset) 175 | line, col = self:sanitize_position(line, col) 176 | col = col + offset 177 | while line > 1 and col < 1 do 178 | line = line - 1 179 | col = col + #self.lines[line] 180 | end 181 | while line < #self.lines and col > #self.lines[line] do 182 | col = col - #self.lines[line] 183 | line = line + 1 184 | end 185 | return self:sanitize_position(line, col) 186 | end 187 | 188 | 189 | local function position_offset_linecol(self, line, col, lineoffset, coloffset) 190 | return self:sanitize_position(line + lineoffset, col + coloffset) 191 | end 192 | 193 | 194 | function Doc:position_offset(line, col, ...) 195 | if type(...) ~= "number" then 196 | return position_offset_func(self, line, col, ...) 197 | elseif select("#", ...) == 1 then 198 | return position_offset_byte(self, line, col, ...) 199 | elseif select("#", ...) == 2 then 200 | return position_offset_linecol(self, line, col, ...) 201 | else 202 | error("bad number of arguments") 203 | end 204 | end 205 | 206 | 207 | function Doc:get_text(line1, col1, line2, col2) 208 | line1, col1 = self:sanitize_position(line1, col1) 209 | line2, col2 = self:sanitize_position(line2, col2) 210 | line1, col1, line2, col2 = sort_positions(line1, col1, line2, col2) 211 | if line1 == line2 then 212 | return self.lines[line1]:sub(col1, col2 - 1) 213 | end 214 | local lines = { self.lines[line1]:sub(col1) } 215 | for i = line1 + 1, line2 - 1 do 216 | table.insert(lines, self.lines[i]) 217 | end 218 | table.insert(lines, self.lines[line2]:sub(1, col2 - 1)) 219 | return table.concat(lines) 220 | end 221 | 222 | 223 | function Doc:get_char(line, col) 224 | line, col = self:sanitize_position(line, col) 225 | return self.lines[line]:sub(col, col) 226 | end 227 | 228 | 229 | local function push_undo(undo_stack, time, type, ...) 230 | undo_stack[undo_stack.idx] = { type = type, time = time, ... } 231 | undo_stack[undo_stack.idx - config.max_undos] = nil 232 | undo_stack.idx = undo_stack.idx + 1 233 | end 234 | 235 | 236 | local function pop_undo(self, undo_stack, redo_stack) 237 | -- pop command 238 | local cmd = undo_stack[undo_stack.idx - 1] 239 | if not cmd then return end 240 | undo_stack.idx = undo_stack.idx - 1 241 | 242 | -- handle command 243 | if cmd.type == "insert" then 244 | local line, col, text = table.unpack(cmd) 245 | self:raw_insert(line, col, text, redo_stack, cmd.time) 246 | 247 | elseif cmd.type == "remove" then 248 | local line1, col1, line2, col2 = table.unpack(cmd) 249 | self:raw_remove(line1, col1, line2, col2, redo_stack, cmd.time) 250 | 251 | elseif cmd.type == "selection" then 252 | self.selection.a.line, self.selection.a.col = cmd[1], cmd[2] 253 | self.selection.b.line, self.selection.b.col = cmd[3], cmd[4] 254 | end 255 | 256 | -- if next undo command is within the merge timeout then treat as a single 257 | -- command and continue to execute it 258 | local next = undo_stack[undo_stack.idx - 1] 259 | if next and math.abs(cmd.time - next.time) < config.undo_merge_timeout then 260 | return pop_undo(self, undo_stack, redo_stack) 261 | end 262 | end 263 | 264 | 265 | function Doc:raw_insert(line, col, text, undo_stack, time) 266 | -- split text into lines and merge with line at insertion point 267 | local lines = split_lines(text) 268 | local before = self.lines[line]:sub(1, col - 1) 269 | local after = self.lines[line]:sub(col) 270 | for i = 1, #lines - 1 do 271 | lines[i] = lines[i] .. "\n" 272 | end 273 | lines[1] = before .. lines[1] 274 | lines[#lines] = lines[#lines] .. after 275 | 276 | -- splice lines into line array 277 | splice(self.lines, line, 1, lines) 278 | 279 | -- push undo 280 | local line2, col2 = self:position_offset(line, col, #text) 281 | push_undo(undo_stack, time, "selection", self:get_selection()) 282 | push_undo(undo_stack, time, "remove", line, col, line2, col2) 283 | 284 | -- update highlighter and assure selection is in bounds 285 | self.highlighter:invalidate(line) 286 | self:sanitize_selection() 287 | end 288 | 289 | 290 | function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time) 291 | -- push undo 292 | local text = self:get_text(line1, col1, line2, col2) 293 | push_undo(undo_stack, time, "selection", self:get_selection()) 294 | push_undo(undo_stack, time, "insert", line1, col1, text) 295 | 296 | -- get line content before/after removed text 297 | local before = self.lines[line1]:sub(1, col1 - 1) 298 | local after = self.lines[line2]:sub(col2) 299 | 300 | -- splice line into line array 301 | splice(self.lines, line1, line2 - line1 + 1, { before .. after }) 302 | 303 | -- update highlighter and assure selection is in bounds 304 | self.highlighter:invalidate(line1) 305 | self:sanitize_selection() 306 | end 307 | 308 | 309 | function Doc:insert(line, col, text) 310 | self.redo_stack = { idx = 1 } 311 | line, col = self:sanitize_position(line, col) 312 | self:raw_insert(line, col, text, self.undo_stack, system.get_time()) 313 | end 314 | 315 | 316 | function Doc:remove(line1, col1, line2, col2) 317 | self.redo_stack = { idx = 1 } 318 | line1, col1 = self:sanitize_position(line1, col1) 319 | line2, col2 = self:sanitize_position(line2, col2) 320 | line1, col1, line2, col2 = sort_positions(line1, col1, line2, col2) 321 | self:raw_remove(line1, col1, line2, col2, self.undo_stack, system.get_time()) 322 | end 323 | 324 | 325 | function Doc:undo() 326 | pop_undo(self, self.undo_stack, self.redo_stack) 327 | end 328 | 329 | 330 | function Doc:redo() 331 | pop_undo(self, self.redo_stack, self.undo_stack) 332 | end 333 | 334 | 335 | function Doc:text_input(text) 336 | if self:has_selection() then 337 | self:delete_to() 338 | end 339 | local line, col = self:get_selection() 340 | self:insert(line, col, text) 341 | self:move_to(#text) 342 | end 343 | 344 | 345 | function Doc:replace(fn) 346 | local line1, col1, line2, col2, swap 347 | local had_selection = self:has_selection() 348 | if had_selection then 349 | line1, col1, line2, col2, swap = self:get_selection(true) 350 | else 351 | line1, col1, line2, col2 = 1, 1, #self.lines, #self.lines[#self.lines] 352 | end 353 | local old_text = self:get_text(line1, col1, line2, col2) 354 | local new_text, n = fn(old_text) 355 | if old_text ~= new_text then 356 | self:insert(line2, col2, new_text) 357 | self:remove(line1, col1, line2, col2) 358 | if had_selection then 359 | line2, col2 = self:position_offset(line1, col1, #new_text) 360 | self:set_selection(line1, col1, line2, col2, swap) 361 | end 362 | end 363 | return n 364 | end 365 | 366 | 367 | function Doc:delete_to(...) 368 | local line, col = self:get_selection(true) 369 | if self:has_selection() then 370 | self:remove(self:get_selection()) 371 | else 372 | local line2, col2 = self:position_offset(line, col, ...) 373 | self:remove(line, col, line2, col2) 374 | line, col = sort_positions(line, col, line2, col2) 375 | end 376 | self:set_selection(line, col) 377 | end 378 | 379 | 380 | function Doc:move_to(...) 381 | local line, col = self:get_selection() 382 | self:set_selection(self:position_offset(line, col, ...)) 383 | end 384 | 385 | 386 | function Doc:select_to(...) 387 | local line, col, line2, col2 = self:get_selection() 388 | line, col = self:position_offset(line, col, ...) 389 | self:set_selection(line, col, line2, col2) 390 | end 391 | 392 | 393 | return Doc 394 | -------------------------------------------------------------------------------- /data/core/docview.lua: -------------------------------------------------------------------------------- 1 | local core = require "core" 2 | local common = require "core.common" 3 | local config = require "core.config" 4 | local style = require "core.style" 5 | local keymap = require "core.keymap" 6 | local translate = require "core.doc.translate" 7 | local View = require "core.view" 8 | 9 | 10 | local DocView = View:extend() 11 | 12 | 13 | local function move_to_line_offset(dv, line, col, offset) 14 | local xo = dv.last_x_offset 15 | if xo.line ~= line or xo.col ~= col then 16 | xo.offset = dv:get_col_x_offset(line, col) 17 | end 18 | xo.line = line + offset 19 | xo.col = dv:get_x_offset_col(line + offset, xo.offset) 20 | return xo.line, xo.col 21 | end 22 | 23 | 24 | DocView.translate = { 25 | ["previous_page"] = function(doc, line, col, dv) 26 | local min, max = dv:get_visible_line_range() 27 | return line - (max - min), 1 28 | end, 29 | 30 | ["next_page"] = function(doc, line, col, dv) 31 | local min, max = dv:get_visible_line_range() 32 | return line + (max - min), 1 33 | end, 34 | 35 | ["previous_line"] = function(doc, line, col, dv) 36 | if line == 1 then 37 | return 1, 1 38 | end 39 | return move_to_line_offset(dv, line, col, -1) 40 | end, 41 | 42 | ["next_line"] = function(doc, line, col, dv) 43 | if line == #doc.lines then 44 | return #doc.lines, math.huge 45 | end 46 | return move_to_line_offset(dv, line, col, 1) 47 | end, 48 | } 49 | 50 | local blink_period = 0.8 51 | 52 | 53 | function DocView:new(doc) 54 | DocView.super.new(self) 55 | self.cursor = "ibeam" 56 | self.scrollable = true 57 | self.doc = assert(doc) 58 | self.font = "code_font" 59 | self.last_x_offset = {} 60 | self.blink_timer = 0 61 | end 62 | 63 | 64 | function DocView:try_close(do_close) 65 | if self.doc:is_dirty() 66 | and #core.get_views_referencing_doc(self.doc) == 1 then 67 | core.command_view:enter("Unsaved Changes; Confirm Close", function(_, item) 68 | if item.text:match("^[cC]") then 69 | do_close() 70 | elseif item.text:match("^[sS]") then 71 | self.doc:save() 72 | do_close() 73 | end 74 | end, function(text) 75 | local items = {} 76 | if not text:find("^[^cC]") then table.insert(items, "Close Without Saving") end 77 | if not text:find("^[^sS]") then table.insert(items, "Save And Close") end 78 | return items 79 | end) 80 | else 81 | do_close() 82 | end 83 | end 84 | 85 | 86 | function DocView:get_name() 87 | local post = self.doc:is_dirty() and "*" or "" 88 | local name = self.doc:get_name() 89 | return name:match("[^/%\\]*$") .. post 90 | end 91 | 92 | 93 | function DocView:get_scrollable_size() 94 | return self:get_line_height() * (#self.doc.lines - 1) + self.size.y 95 | end 96 | 97 | 98 | function DocView:get_font() 99 | return style[self.font] 100 | end 101 | 102 | 103 | function DocView:get_line_height() 104 | return math.floor(self:get_font():get_height() * config.line_height) 105 | end 106 | 107 | 108 | function DocView:get_gutter_width() 109 | return self:get_font():get_width(#self.doc.lines) + style.padding.x * 2 110 | end 111 | 112 | 113 | function DocView:get_line_screen_position(idx) 114 | local x, y = self:get_content_offset() 115 | local lh = self:get_line_height() 116 | local gw = self:get_gutter_width() 117 | return x + gw, y + (idx-1) * lh + style.padding.y 118 | end 119 | 120 | 121 | function DocView:get_line_text_y_offset() 122 | local lh = self:get_line_height() 123 | local th = self:get_font():get_height() 124 | return (lh - th) / 2 125 | end 126 | 127 | 128 | function DocView:get_visible_line_range() 129 | local x, y, x2, y2 = self:get_content_bounds() 130 | local lh = self:get_line_height() 131 | local minline = math.max(1, math.floor(y / lh)) 132 | local maxline = math.min(#self.doc.lines, math.floor(y2 / lh) + 1) 133 | return minline, maxline 134 | end 135 | 136 | 137 | function DocView:get_col_x_offset(line, col) 138 | local text = self.doc.lines[line] 139 | if not text then return 0 end 140 | return self:get_font():get_width(text:sub(1, col - 1)) 141 | end 142 | 143 | 144 | function DocView:get_x_offset_col(line, x) 145 | local text = self.doc.lines[line] 146 | 147 | local xoffset, last_i, i = 0, 1, 1 148 | for char in common.utf8_chars(text) do 149 | local w = self:get_font():get_width(char) 150 | if xoffset >= x then 151 | return (xoffset - x > w / 2) and last_i or i 152 | end 153 | xoffset = xoffset + w 154 | last_i = i 155 | i = i + #char 156 | end 157 | 158 | return #text 159 | end 160 | 161 | 162 | function DocView:resolve_screen_position(x, y) 163 | local ox, oy = self:get_line_screen_position(1) 164 | local line = math.floor((y - oy) / self:get_line_height()) + 1 165 | line = common.clamp(line, 1, #self.doc.lines) 166 | local col = self:get_x_offset_col(line, x - ox) 167 | return line, col 168 | end 169 | 170 | 171 | function DocView:scroll_to_line(line, ignore_if_visible, instant) 172 | local min, max = self:get_visible_line_range() 173 | if not (ignore_if_visible and line > min and line < max) then 174 | local lh = self:get_line_height() 175 | self.scroll.to.y = math.max(0, lh * (line - 1) - self.size.y / 2) 176 | if instant then 177 | self.scroll.y = self.scroll.to.y 178 | end 179 | end 180 | end 181 | 182 | 183 | function DocView:scroll_to_make_visible(line, col) 184 | local min = self:get_line_height() * (line - 1) 185 | local max = self:get_line_height() * (line + 2) - self.size.y 186 | self.scroll.to.y = math.min(self.scroll.to.y, min) 187 | self.scroll.to.y = math.max(self.scroll.to.y, max) 188 | local gw = self:get_gutter_width() 189 | local xoffset = self:get_col_x_offset(line, col) 190 | local max = xoffset - self.size.x + gw + self.size.x / 5 191 | self.scroll.to.x = math.max(0, max) 192 | end 193 | 194 | 195 | local function mouse_selection(doc, clicks, line1, col1, line2, col2) 196 | local swap = line2 < line1 or line2 == line1 and col2 <= col1 197 | if swap then 198 | line1, col1, line2, col2 = line2, col2, line1, col1 199 | end 200 | if clicks == 2 then 201 | line1, col1 = translate.start_of_word(doc, line1, col1) 202 | line2, col2 = translate.end_of_word(doc, line2, col2) 203 | elseif clicks == 3 then 204 | if line2 == #doc.lines and doc.lines[#doc.lines] ~= "\n" then 205 | doc:insert(math.huge, math.huge, "\n") 206 | end 207 | line1, col1, line2, col2 = line1, 1, line2 + 1, 1 208 | end 209 | if swap then 210 | return line2, col2, line1, col1 211 | end 212 | return line1, col1, line2, col2 213 | end 214 | 215 | 216 | function DocView:on_mouse_pressed(button, x, y, clicks) 217 | local caught = DocView.super.on_mouse_pressed(self, button, x, y, clicks) 218 | if caught then 219 | return 220 | end 221 | if keymap.modkeys["shift"] then 222 | if clicks == 1 then 223 | local line1, col1 = select(3, self.doc:get_selection()) 224 | local line2, col2 = self:resolve_screen_position(x, y) 225 | self.doc:set_selection(line2, col2, line1, col1) 226 | end 227 | else 228 | local line, col = self:resolve_screen_position(x, y) 229 | self.doc:set_selection(mouse_selection(self.doc, clicks, line, col, line, col)) 230 | self.mouse_selecting = { line, col, clicks = clicks } 231 | end 232 | self.blink_timer = 0 233 | end 234 | 235 | 236 | function DocView:on_mouse_moved(x, y, ...) 237 | DocView.super.on_mouse_moved(self, x, y, ...) 238 | 239 | if self:scrollbar_overlaps_point(x, y) or self.dragging_scrollbar then 240 | self.cursor = "arrow" 241 | else 242 | self.cursor = "ibeam" 243 | end 244 | 245 | if self.mouse_selecting then 246 | local l1, c1 = self:resolve_screen_position(x, y) 247 | local l2, c2 = table.unpack(self.mouse_selecting) 248 | local clicks = self.mouse_selecting.clicks 249 | self.doc:set_selection(mouse_selection(self.doc, clicks, l1, c1, l2, c2)) 250 | end 251 | end 252 | 253 | 254 | function DocView:on_mouse_released(button) 255 | DocView.super.on_mouse_released(self, button) 256 | self.mouse_selecting = nil 257 | end 258 | 259 | 260 | function DocView:on_text_input(text) 261 | self.doc:text_input(text) 262 | end 263 | 264 | 265 | function DocView:update() 266 | -- scroll to make caret visible and reset blink timer if it moved 267 | local line, col = self.doc:get_selection() 268 | if (line ~= self.last_line or col ~= self.last_col) and self.size.x > 0 then 269 | if core.active_view == self then 270 | self:scroll_to_make_visible(line, col) 271 | end 272 | self.blink_timer = 0 273 | self.last_line, self.last_col = line, col 274 | end 275 | 276 | -- update blink timer 277 | if self == core.active_view and not self.mouse_selecting then 278 | local n = blink_period / 2 279 | local prev = self.blink_timer 280 | self.blink_timer = (self.blink_timer + 1 / config.fps) % blink_period 281 | if (self.blink_timer > n) ~= (prev > n) then 282 | core.redraw = true 283 | end 284 | end 285 | 286 | DocView.super.update(self) 287 | end 288 | 289 | 290 | function DocView:draw_line_highlight(x, y) 291 | local lh = self:get_line_height() 292 | renderer.draw_rect(x, y, self.size.x, lh, style.line_highlight) 293 | end 294 | 295 | 296 | function DocView:draw_line_text(idx, x, y) 297 | local tx, ty = x, y + self:get_line_text_y_offset() 298 | local font = self:get_font() 299 | for _, type, text in self.doc.highlighter:each_token(idx) do 300 | local color = style.syntax[type] 301 | tx = renderer.draw_text(font, text, tx, ty, color) 302 | end 303 | end 304 | 305 | 306 | function DocView:draw_line_body(idx, x, y) 307 | local line, col = self.doc:get_selection() 308 | 309 | -- draw selection if it overlaps this line 310 | local line1, col1, line2, col2 = self.doc:get_selection(true) 311 | if idx >= line1 and idx <= line2 then 312 | local text = self.doc.lines[idx] 313 | if line1 ~= idx then col1 = 1 end 314 | if line2 ~= idx then col2 = #text + 1 end 315 | local x1 = x + self:get_col_x_offset(idx, col1) 316 | local x2 = x + self:get_col_x_offset(idx, col2) 317 | local lh = self:get_line_height() 318 | renderer.draw_rect(x1, y, x2 - x1, lh, style.selection) 319 | end 320 | 321 | -- draw line highlight if caret is on this line 322 | if config.highlight_current_line and not self.doc:has_selection() 323 | and line == idx and core.active_view == self then 324 | self:draw_line_highlight(x + self.scroll.x, y) 325 | end 326 | 327 | -- draw line's text 328 | self:draw_line_text(idx, x, y) 329 | 330 | -- draw caret if it overlaps this line 331 | if line == idx and core.active_view == self 332 | and self.blink_timer < blink_period / 2 333 | and system.window_has_focus() then 334 | local lh = self:get_line_height() 335 | local x1 = x + self:get_col_x_offset(line, col) 336 | renderer.draw_rect(x1, y, style.caret_width, lh, style.caret) 337 | end 338 | end 339 | 340 | 341 | function DocView:draw_line_gutter(idx, x, y) 342 | local color = style.line_number 343 | local line1, _, line2, _ = self.doc:get_selection(true) 344 | if idx >= line1 and idx <= line2 then 345 | color = style.line_number2 346 | end 347 | local yoffset = self:get_line_text_y_offset() 348 | x = x + style.padding.x 349 | renderer.draw_text(self:get_font(), idx, x, y + yoffset, color) 350 | end 351 | 352 | 353 | function DocView:draw() 354 | self:draw_background(style.background) 355 | 356 | local font = self:get_font() 357 | font:set_tab_width(font:get_width(" ") * config.indent_size) 358 | 359 | local minline, maxline = self:get_visible_line_range() 360 | local lh = self:get_line_height() 361 | 362 | local _, y = self:get_line_screen_position(minline) 363 | local x = self.position.x 364 | for i = minline, maxline do 365 | self:draw_line_gutter(i, x, y) 366 | y = y + lh 367 | end 368 | 369 | local x, y = self:get_line_screen_position(minline) 370 | local gw = self:get_gutter_width() 371 | local pos = self.position 372 | core.push_clip_rect(pos.x + gw, pos.y, self.size.x, self.size.y) 373 | for i = minline, maxline do 374 | self:draw_line_body(i, x, y) 375 | y = y + lh 376 | end 377 | core.pop_clip_rect() 378 | 379 | self:draw_scrollbar() 380 | end 381 | 382 | 383 | return DocView 384 | -------------------------------------------------------------------------------- /data/core/init.lua: -------------------------------------------------------------------------------- 1 | require "core.strict" 2 | local common = require "core.common" 3 | local config = require "core.config" 4 | local style = require "core.style" 5 | local command 6 | local keymap 7 | local RootView 8 | local StatusView 9 | local CommandView 10 | local Doc 11 | 12 | local core = {} 13 | 14 | core.quit_request = false 15 | 16 | local function project_scan_thread() 17 | local function diff_files(a, b) 18 | if #a ~= #b then return true end 19 | for i, v in ipairs(a) do 20 | if b[i].filename ~= v.filename 21 | or b[i].modified ~= v.modified then 22 | return true 23 | end 24 | end 25 | end 26 | 27 | local function compare_file(a, b) 28 | return a.filename < b.filename 29 | end 30 | 31 | local function get_files(path, t) 32 | coroutine.yield() 33 | t = t or {} 34 | local size_limit = config.file_size_limit * 10e5 35 | local all = system.list_dir(path) or {} 36 | local dirs, files = {}, {} 37 | 38 | for _, file in ipairs(all) do 39 | if not common.match_pattern(file, config.ignore_files) then 40 | local file = (path ~= "." and path .. PATHSEP or "") .. file 41 | local info = system.get_file_info(file) 42 | if info and info.size < size_limit then 43 | info.filename = file 44 | table.insert(info.type == "dir" and dirs or files, info) 45 | end 46 | end 47 | end 48 | 49 | table.sort(dirs, compare_file) 50 | for _, f in ipairs(dirs) do 51 | table.insert(t, f) 52 | get_files(f.filename, t) 53 | end 54 | 55 | table.sort(files, compare_file) 56 | for _, f in ipairs(files) do 57 | table.insert(t, f) 58 | end 59 | 60 | return t 61 | end 62 | 63 | while true do 64 | -- get project files and replace previous table if the new table is 65 | -- different 66 | local t = get_files(".") 67 | if diff_files(core.project_files, t) then 68 | core.project_files = t 69 | core.redraw = true 70 | end 71 | 72 | -- wait for next scan 73 | coroutine.yield(config.project_scan_rate) 74 | end 75 | end 76 | 77 | 78 | function core.init() 79 | command = require "core.command" 80 | keymap = require "core.keymap" 81 | RootView = require "core.rootview" 82 | StatusView = require "core.statusview" 83 | CommandView = require "core.commandview" 84 | Doc = require "core.doc" 85 | 86 | local project_dir = EXEDIR 87 | local files = {} 88 | for i = 2, #ARGS do 89 | local info = system.get_file_info(ARGS[i]) or {} 90 | if info.type == "file" then 91 | table.insert(files, system.absolute_path(ARGS[i])) 92 | elseif info.type == "dir" then 93 | project_dir = ARGS[i] 94 | end 95 | end 96 | 97 | system.chdir(project_dir) 98 | 99 | core.frame_start = 0 100 | core.clip_rect_stack = {{ 0,0,0,0 }} 101 | core.log_items = {} 102 | core.docs = {} 103 | core.threads = setmetatable({}, { __mode = "k" }) 104 | core.project_files = {} 105 | core.redraw = true 106 | 107 | core.root_view = RootView() 108 | core.command_view = CommandView() 109 | core.status_view = StatusView() 110 | 111 | core.root_view.root_node:split("down", core.command_view, true) 112 | core.root_view.root_node.b:split("down", core.status_view, true) 113 | 114 | core.add_thread(project_scan_thread) 115 | command.add_defaults() 116 | local got_plugin_error = not core.load_plugins() 117 | local got_user_error = not core.try(require, "user") 118 | local got_project_error = not core.load_project_module() 119 | 120 | for _, filename in ipairs(files) do 121 | core.root_view:open_doc(core.open_doc(filename)) 122 | end 123 | 124 | if got_plugin_error or got_user_error or got_project_error then 125 | command.perform("core:open-log") 126 | end 127 | end 128 | 129 | 130 | local temp_uid = math.floor(system.get_time() * 1000) % 0xffffffff 131 | local temp_file_prefix = string.format(".lite_temp_%08x", tonumber(temp_uid)) 132 | local temp_file_counter = 0 133 | 134 | local function delete_temp_files() 135 | for _, filename in ipairs(system.list_dir(EXEDIR)) do 136 | if filename:find(temp_file_prefix, 1, true) == 1 then 137 | os.remove(EXEDIR .. PATHSEP .. filename) 138 | end 139 | end 140 | end 141 | 142 | function core.temp_filename(ext) 143 | temp_file_counter = temp_file_counter + 1 144 | return EXEDIR .. PATHSEP .. temp_file_prefix 145 | .. string.format("%06x", temp_file_counter) .. (ext or "") 146 | end 147 | 148 | 149 | function core.quit(force) 150 | if force then 151 | delete_temp_files() 152 | core.quit_request = true 153 | return 154 | end 155 | local dirty_count = 0 156 | local dirty_name 157 | for _, doc in ipairs(core.docs) do 158 | if doc:is_dirty() then 159 | dirty_count = dirty_count + 1 160 | dirty_name = doc:get_name() 161 | end 162 | end 163 | if dirty_count > 0 then 164 | local text 165 | if dirty_count == 1 then 166 | text = string.format("\"%s\" has unsaved changes. Quit anyway?", dirty_name) 167 | else 168 | text = string.format("%d docs have unsaved changes. Quit anyway?", dirty_count) 169 | end 170 | local confirm = system.show_confirm_dialog("Unsaved Changes", text) 171 | if not confirm then return end 172 | end 173 | core.quit(true) 174 | end 175 | 176 | 177 | function core.load_plugins() 178 | local no_errors = true 179 | local files = system.list_dir(EXEDIR .. "/data/plugins") 180 | for _, filename in ipairs(files) do 181 | local modname = "plugins." .. filename:gsub(".lua$", "") 182 | local ok = core.try(require, modname) 183 | if ok then 184 | core.log_quiet("Loaded plugin %q", modname) 185 | else 186 | no_errors = false 187 | end 188 | end 189 | return no_errors 190 | end 191 | 192 | 193 | function core.load_project_module() 194 | local filename = ".lite_project.lua" 195 | if system.get_file_info(filename) then 196 | return core.try(function() 197 | local fn, err = loadfile(filename) 198 | if not fn then error("Error when loading project module:\n\t" .. err) end 199 | fn() 200 | core.log_quiet("Loaded project module") 201 | end) 202 | end 203 | return true 204 | end 205 | 206 | 207 | function core.reload_module(name) 208 | local old = package.loaded[name] 209 | package.loaded[name] = nil 210 | local new = require(name) 211 | if type(old) == "table" then 212 | for k, v in pairs(new) do old[k] = v end 213 | package.loaded[name] = old 214 | end 215 | end 216 | 217 | 218 | function core.set_active_view(view) 219 | assert(view, "Tried to set active view to nil") 220 | if view ~= core.active_view then 221 | core.last_active_view = core.active_view 222 | core.active_view = view 223 | end 224 | end 225 | 226 | 227 | function core.add_thread(f, weak_ref) 228 | local key = weak_ref or #core.threads + 1 229 | local fn = function() return core.try(f) end 230 | core.threads[key] = { cr = coroutine.create(fn), wake = 0 } 231 | end 232 | 233 | 234 | function core.push_clip_rect(x, y, w, h) 235 | local x2, y2, w2, h2 = table.unpack(core.clip_rect_stack[#core.clip_rect_stack]) 236 | local r, b, r2, b2 = x+w, y+h, x2+w2, y2+h2 237 | x, y = math.max(x, x2), math.max(y, y2) 238 | b, r = math.min(b, b2), math.min(r, r2) 239 | w, h = r-x, b-y 240 | table.insert(core.clip_rect_stack, { x, y, w, h }) 241 | renderer.set_clip_rect(x, y, w, h) 242 | end 243 | 244 | 245 | function core.pop_clip_rect() 246 | table.remove(core.clip_rect_stack) 247 | local x, y, w, h = table.unpack(core.clip_rect_stack[#core.clip_rect_stack]) 248 | renderer.set_clip_rect(x, y, w, h) 249 | end 250 | 251 | 252 | function core.open_doc(filename) 253 | if filename then 254 | -- try to find existing doc for filename 255 | local abs_filename = system.absolute_path(filename) 256 | for _, doc in ipairs(core.docs) do 257 | if doc.filename 258 | and abs_filename == system.absolute_path(doc.filename) then 259 | return doc 260 | end 261 | end 262 | end 263 | -- no existing doc for filename; create new 264 | local doc = Doc(filename) 265 | table.insert(core.docs, doc) 266 | core.log_quiet(filename and "Opened doc \"%s\"" or "Opened new doc", filename) 267 | return doc 268 | end 269 | 270 | 271 | function core.get_views_referencing_doc(doc) 272 | local res = {} 273 | local views = core.root_view.root_node:get_children() 274 | for _, view in ipairs(views) do 275 | if view.doc == doc then table.insert(res, view) end 276 | end 277 | return res 278 | end 279 | 280 | 281 | local function log(icon, icon_color, fmt, ...) 282 | local text = string.format(fmt, ...) 283 | if icon then 284 | core.status_view:show_message(icon, icon_color, text) 285 | end 286 | 287 | local info = debug.getinfo(2, "Sl") 288 | local at = string.format("%s:%d", info.short_src, info.currentline) 289 | local item = { text = text, time = os.time(), at = at } 290 | table.insert(core.log_items, item) 291 | if #core.log_items > config.max_log_items then 292 | table.remove(core.log_items, 1) 293 | end 294 | return item 295 | end 296 | 297 | 298 | function core.log(...) 299 | return log("i", style.text, ...) 300 | end 301 | 302 | 303 | function core.log_quiet(...) 304 | return log(nil, nil, ...) 305 | end 306 | 307 | 308 | function core.error(...) 309 | return log("!", style.accent, ...) 310 | end 311 | 312 | 313 | function core.try(fn, ...) 314 | local err 315 | local ok, res = xpcall(fn, function(msg) 316 | local item = core.error("%s", msg) 317 | item.info = debug.traceback(nil, 2):gsub("\t", "") 318 | err = msg 319 | end, ...) 320 | if ok then 321 | return true, res 322 | end 323 | return false, err 324 | end 325 | 326 | 327 | function core.on_event(type, ...) 328 | local did_keymap = false 329 | if type == "textinput" then 330 | core.root_view:on_text_input(...) 331 | elseif type == "keypressed" then 332 | did_keymap = keymap.on_key_pressed(...) 333 | elseif type == "keyreleased" then 334 | keymap.on_key_released(...) 335 | elseif type == "mousemoved" then 336 | core.root_view:on_mouse_moved(...) 337 | elseif type == "mousepressed" then 338 | core.root_view:on_mouse_pressed(...) 339 | elseif type == "mousereleased" then 340 | core.root_view:on_mouse_released(...) 341 | elseif type == "mousewheel" then 342 | core.root_view:on_mouse_wheel(...) 343 | elseif type == "filedropped" then 344 | local filename, mx, my = ... 345 | local info = system.get_file_info(filename) 346 | if info and info.type == "dir" then 347 | system.exec(string.format("%q %q", EXEFILE, filename)) 348 | else 349 | local ok, doc = core.try(core.open_doc, filename) 350 | if ok then 351 | local node = core.root_view.root_node:get_child_overlapping_point(mx, my) 352 | node:set_active_view(node.active_view) 353 | core.root_view:open_doc(doc) 354 | end 355 | end 356 | elseif type == "quit" then 357 | core.quit() 358 | end 359 | return did_keymap 360 | end 361 | 362 | 363 | function core.step() 364 | -- handle events 365 | local did_keymap = false 366 | local mouse_moved = false 367 | local mouse = { x = 0, y = 0, dx = 0, dy = 0 } 368 | 369 | for type, a,b,c,d in system.poll_event do 370 | if type == "mousemoved" then 371 | mouse_moved = true 372 | mouse.x, mouse.y = a, b 373 | mouse.dx, mouse.dy = mouse.dx + c, mouse.dy + d 374 | elseif type == "textinput" and did_keymap then 375 | did_keymap = false 376 | else 377 | local _, res = core.try(core.on_event, type, a, b, c, d) 378 | did_keymap = res or did_keymap 379 | end 380 | core.redraw = true 381 | end 382 | if mouse_moved then 383 | core.try(core.on_event, "mousemoved", mouse.x, mouse.y, mouse.dx, mouse.dy) 384 | end 385 | 386 | local width, height = renderer.get_size() 387 | 388 | -- update 389 | core.root_view.size.x, core.root_view.size.y = width, height 390 | core.root_view:update() 391 | if not core.redraw then return false end 392 | core.redraw = false 393 | 394 | -- close unreferenced docs 395 | for i = #core.docs, 1, -1 do 396 | local doc = core.docs[i] 397 | if #core.get_views_referencing_doc(doc) == 0 then 398 | table.remove(core.docs, i) 399 | core.log_quiet("Closed doc \"%s\"", doc:get_name()) 400 | end 401 | end 402 | 403 | -- update window title 404 | local name = core.active_view:get_name() 405 | local title = (name ~= "---") and (name .. " - lite") or "lite" 406 | if title ~= core.window_title then 407 | system.set_window_title(title) 408 | core.window_title = title 409 | end 410 | 411 | -- draw 412 | renderer.begin_frame() 413 | core.clip_rect_stack[1] = { 0, 0, width, height } 414 | renderer.set_clip_rect(table.unpack(core.clip_rect_stack[1])) 415 | core.root_view:draw() 416 | renderer.end_frame() 417 | return true 418 | end 419 | 420 | 421 | local run_threads = coroutine.wrap(function() 422 | while true do 423 | local max_time = 1 / config.fps - 0.004 424 | local ran_any_threads = false 425 | 426 | for k, thread in pairs(core.threads) do 427 | -- run thread 428 | if thread.wake < system.get_time() then 429 | local _, wait = assert(coroutine.resume(thread.cr)) 430 | if coroutine.status(thread.cr) == "dead" then 431 | if type(k) == "number" then 432 | table.remove(core.threads, k) 433 | else 434 | core.threads[k] = nil 435 | end 436 | elseif wait then 437 | thread.wake = system.get_time() + wait 438 | end 439 | ran_any_threads = true 440 | end 441 | 442 | -- stop running threads if we're about to hit the end of frame 443 | if system.get_time() - core.frame_start > max_time then 444 | coroutine.yield() 445 | end 446 | end 447 | 448 | if not ran_any_threads then coroutine.yield() end 449 | end 450 | end) 451 | 452 | 453 | function core.run() 454 | while true do 455 | if core.quit_request then 456 | break 457 | end 458 | core.frame_start = system.get_time() 459 | local did_redraw = core.step() 460 | run_threads() 461 | if not did_redraw and not system.window_has_focus() then 462 | system.wait_event(0.25) 463 | end 464 | local elapsed = system.get_time() - core.frame_start 465 | system.sleep(math.max(0, 1 / config.fps - elapsed)) 466 | end 467 | end 468 | 469 | 470 | function core.on_error(err) 471 | -- write error to file 472 | local fp = io.open(EXEDIR .. "/error.txt", "wb") 473 | fp:write("Error: " .. tostring(err) .. "\n") 474 | fp:write(debug.traceback(nil, 4)) 475 | fp:close() 476 | -- save copy of all unsaved documents 477 | for _, doc in ipairs(core.docs) do 478 | if doc:is_dirty() and doc.filename then 479 | doc:save(doc.filename .. "~") 480 | end 481 | end 482 | end 483 | 484 | 485 | return core 486 | -------------------------------------------------------------------------------- /api_system.odin: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "base:runtime" 4 | import "core:bufio" 5 | import "core:c/libc" 6 | import "core:fmt" 7 | import "core:os" 8 | import "core:path/filepath" 9 | import "core:strings" 10 | import "core:time" 11 | 12 | import "core:sys/windows" 13 | _ :: windows 14 | 15 | import lua "vendor:lua/5.4" 16 | import sdl "vendor:sdl2" 17 | 18 | search_file_find :: proc "c" (L: ^lua.State) -> i32 { 19 | 20 | context = runtime.default_context() 21 | pattern := string(lua.L_checkstring(L, 1)) 22 | file := string(lua.L_checkstring(L, 2)) 23 | 24 | f, ferr := os.open(file) 25 | if ferr != nil { 26 | // handle error appropriately 27 | fmt.println("failed to open ", file) 28 | return 0 29 | } 30 | defer os.close(f) 31 | 32 | r: bufio.Reader 33 | buffer: [65536]byte 34 | bufio.reader_init_with_buf(&r, os.stream_from_handle(f), buffer[:]) 35 | defer bufio.reader_destroy(&r) 36 | 37 | pat_u8 := transmute([]u8)(pattern) 38 | 39 | line_no := 1 40 | num_results: i32 = 0 41 | for { 42 | // This will allocate a string because the line might go over the backing 43 | // buffer and thus need to join things together 44 | line, err := bufio.reader_read_slice(&r, '\n') 45 | 46 | if err != nil { 47 | if err == .EOF || err == .Unknown { 48 | break 49 | } 50 | // TODO handle longer lines 51 | } 52 | pos := strcasestr(line, pat_u8) 53 | if pos >= 0 { 54 | lua.createtable(L, 0, 4) 55 | 56 | lua.pushstring(L, strings.clone_to_cstring(file)) 57 | lua.setfield(L, -2, "file") 58 | 59 | lua.pushstring(L, strings.clone_to_cstring(string(line))) 60 | lua.setfield(L, -2, "text") 61 | 62 | lua.pushinteger(L, lua.Integer(line_no)) 63 | lua.setfield(L, -2, "line") 64 | 65 | lua.pushinteger(L, lua.Integer(pos + 1)) 66 | lua.setfield(L, -2, "col") 67 | // fmt.println("Found match at:", line_no, pos + 1, "in:", file) 68 | num_results += 1 69 | } 70 | 71 | line_no += 1 72 | } 73 | return num_results 74 | } 75 | 76 | strcasestr :: proc(h: []u8, n: []u8) -> int { 77 | lower :: #force_inline proc "contextless" (ch: byte) -> byte {return ('a' - 'A') | ch} 78 | if len(n) == 0 do return 0 79 | 80 | first := lower(n[0]) 81 | for i := 0; i < len(h); i += 1 { 82 | if lower(h[i]) == first { 83 | n_idx := 1 84 | h_idx := i + 1 85 | for { 86 | // needle exhausted, we found the str 87 | if n_idx >= len(n) do return i 88 | // haystack exhausted? 89 | if h_idx >= len(h) do break 90 | // mismatch, break and try again 91 | if lower(h[h_idx]) != lower(n[n_idx]) do break 92 | // move to next char 93 | n_idx += 1 94 | h_idx += 1 95 | } 96 | } 97 | } 98 | return -1 99 | } 100 | 101 | button_name :: proc(button: u8) -> cstring { 102 | switch (button) { 103 | case 1: 104 | return "left" 105 | case 2: 106 | return "middle" 107 | case 3: 108 | return "right" 109 | case: 110 | return "?" 111 | } 112 | } 113 | 114 | key_name :: proc(dst: []u8, sym: sdl.Keycode) -> cstring { 115 | keyname: string = string(sdl.GetKeyName(sym)) 116 | 117 | i := 0 118 | for c in keyname { 119 | dst[i] = cast(u8)libc.tolower(i32(c)) 120 | i += 1 121 | } 122 | return cstring(raw_data(dst)) 123 | } 124 | 125 | f_poll_event :: proc "c" (L: ^lua.State) -> i32 { 126 | context = runtime.default_context() 127 | buf: [16]u8 128 | mx, my, wx, wy: i32 129 | e: sdl.Event 130 | 131 | if (!sdl.PollEvent(&e)) { 132 | return 0 133 | } 134 | 135 | #partial switch (e.type) { 136 | case .QUIT: 137 | lua.pushstring(L, "quit") 138 | return 1 139 | 140 | case .WINDOWEVENT: 141 | if (e.window.event == sdl.WindowEventID.RESIZED) { 142 | lua.pushstring(L, "resized") 143 | lua.pushinteger(L, lua.Integer(e.window.data1)) 144 | lua.pushinteger(L, lua.Integer(e.window.data2)) 145 | return 3 146 | } else if (e.window.event == sdl.WindowEventID.EXPOSED) { 147 | rencache_invalidate() 148 | lua.pushstring(L, "exposed") 149 | return 1 150 | } 151 | /* on some systems, when alt-tabbing to the window SDL will queue up 152 | * several KEYDOWN events for the `tab` key; we flush all keydown 153 | * events on focus so these are discarded */ 154 | if (e.window.event == sdl.WindowEventID.FOCUS_GAINED) { 155 | sdl.FlushEvent(sdl.EventType.KEYDOWN) 156 | } 157 | return f_poll_event(L) 158 | 159 | case .DROPFILE: 160 | sdl.GetGlobalMouseState(&mx, &my) 161 | sdl.GetWindowPosition(window, &wx, &wy) 162 | lua.pushstring(L, "filedropped") 163 | lua.pushstring(L, e.drop.file) 164 | lua.pushinteger(L, cast(lua.Integer)(mx - wx)) 165 | lua.pushinteger(L, cast(lua.Integer)(my - wy)) 166 | sdl.free(cast([^]u8)e.drop.file) 167 | return 4 168 | 169 | case .KEYDOWN: 170 | lua.pushstring(L, "keypressed") 171 | lua.pushstring(L, key_name(buf[:], e.key.keysym.sym)) 172 | return 2 173 | 174 | case .KEYUP: 175 | lua.pushstring(L, "keyreleased") 176 | lua.pushstring(L, key_name(buf[:], e.key.keysym.sym)) 177 | return 2 178 | 179 | case .TEXTINPUT: 180 | lua.pushstring(L, "textinput") 181 | lua.pushstring(L, cstring(raw_data(e.text.text[:]))) 182 | return 2 183 | 184 | case .MOUSEBUTTONDOWN: 185 | if (e.button.button == 1) { 186 | sdl.CaptureMouse(true) 187 | } 188 | lua.pushstring(L, "mousepressed") 189 | lua.pushstring(L, button_name(e.button.button)) 190 | lua.pushinteger(L, cast(lua.Integer)e.button.x) 191 | lua.pushinteger(L, cast(lua.Integer)e.button.y) 192 | lua.pushinteger(L, cast(lua.Integer)e.button.clicks) 193 | return 5 194 | 195 | case .MOUSEBUTTONUP: 196 | if (e.button.button == 1) { 197 | sdl.CaptureMouse(false) 198 | } 199 | lua.pushstring(L, "mousereleased") 200 | lua.pushstring(L, button_name(e.button.button)) 201 | lua.pushinteger(L, cast(lua.Integer)e.button.x) 202 | lua.pushinteger(L, cast(lua.Integer)e.button.y) 203 | return 4 204 | 205 | case .MOUSEMOTION: 206 | lua.pushstring(L, "mousemoved") 207 | lua.pushinteger(L, cast(lua.Integer)e.motion.x) 208 | lua.pushinteger(L, cast(lua.Integer)e.motion.y) 209 | lua.pushinteger(L, cast(lua.Integer)e.motion.xrel) 210 | lua.pushinteger(L, cast(lua.Integer)e.motion.yrel) 211 | return 5 212 | 213 | case .MOUSEWHEEL: 214 | lua.pushstring(L, "mousewheel") 215 | lua.pushinteger(L, cast(lua.Integer)e.wheel.y) 216 | return 2 217 | 218 | case: 219 | return f_poll_event(L) 220 | } 221 | 222 | return 0 223 | } 224 | 225 | f_wait_event :: proc "c" (L: ^lua.State) -> i32 { 226 | n: libc.int = cast(libc.int)lua.L_checknumber(L, 1) 227 | lua.pushboolean(L, cast(b32)sdl.WaitEventTimeout(nil, n * 1000)) 228 | return 1 229 | } 230 | 231 | cursor_cache: [sdl.SystemCursor.NUM_SYSTEM_CURSORS]^sdl.Cursor 232 | 233 | cursor_opts := []cstring{"arrow", "ibeam", "sizeh", "sizev", "hand"} 234 | 235 | cursor_enums := []sdl.SystemCursor { 236 | sdl.SystemCursor.ARROW, 237 | sdl.SystemCursor.IBEAM, 238 | sdl.SystemCursor.SIZEWE, 239 | sdl.SystemCursor.SIZENS, 240 | sdl.SystemCursor.HAND, 241 | } 242 | 243 | f_set_cursor :: proc "c" (L: ^lua.State) -> i32 { 244 | opt: i32 = lua.L_checkoption(L, 1, "arrow", raw_data(cursor_opts)) 245 | cursor_value := cursor_enums[opt] 246 | n: i32 = cast(i32)cursor_value 247 | cursor: ^sdl.Cursor = cursor_cache[n] 248 | if (cursor == nil) { 249 | cursor = sdl.CreateSystemCursor(cursor_value) 250 | cursor_cache[n] = cursor 251 | } 252 | sdl.SetCursor(cursor) 253 | return 0 254 | } 255 | 256 | f_set_window_title :: proc "c" (L: ^lua.State) -> i32 { 257 | title := lua.L_checkstring(L, 1) 258 | sdl.SetWindowTitle(window, title) 259 | return 0 260 | } 261 | 262 | window_opts := []cstring{"normal", "maximized", "fullscreen"} 263 | Win :: enum { 264 | WIN_NORMAL, 265 | WIN_MAXIMIZED, 266 | WIN_FULLSCREEN, 267 | } 268 | 269 | f_set_window_mode :: proc "c" (L: ^lua.State) -> i32 { 270 | n := lua.L_checkoption(L, 1, "normal", raw_data(window_opts)) 271 | sdl.SetWindowFullscreen( 272 | window, 273 | n == cast(i32)Win.WIN_FULLSCREEN ? sdl.WINDOW_FULLSCREEN_DESKTOP : sdl.WindowFlags{}, 274 | ) 275 | if (n == cast(i32)Win.WIN_NORMAL) {sdl.RestoreWindow(window)} 276 | if (n == cast(i32)Win.WIN_MAXIMIZED) {sdl.MaximizeWindow(window)} 277 | return 0 278 | } 279 | 280 | 281 | f_window_has_focus :: proc "c" (L: ^lua.State) -> i32 { 282 | flags := sdl.GetWindowFlags(window) 283 | lua.pushboolean(L, cast(b32)(flags & cast(u32)sdl.WINDOW_INPUT_FOCUS)) 284 | return 1 285 | } 286 | 287 | f_show_confirm_dialog :: proc "c" (L: ^lua.State) -> i32 { 288 | title: cstring = lua.L_checkstring(L, 1) 289 | msg: cstring = lua.L_checkstring(L, 2) 290 | 291 | when ODIN_OS == .Windows { 292 | context = runtime.default_context() 293 | message := windows.utf8_to_wstring(string(msg)) 294 | caption := windows.utf8_to_wstring(string(title)) 295 | id := windows.MessageBoxW(windows.HWND(nil), message, caption, windows.UINT(windows.MB_YESNO | windows.MB_ICONWARNING)) 296 | lua.pushboolean(L, cast(b32)(id == windows.IDYES)) 297 | } else { 298 | buttons := []sdl.MessageBoxButtonData { 299 | {sdl.MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, 1, "Yes"}, 300 | {sdl.MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT, 0, "No"}, 301 | } 302 | data: sdl.MessageBoxData = { 303 | title = title, 304 | message = msg, 305 | numbuttons = 2, 306 | buttons = raw_data(buttons), 307 | } 308 | buttonid: i32 309 | sdl.ShowMessageBox(&data, &buttonid) 310 | lua.pushboolean(L, buttonid == 1) 311 | } 312 | return 1 313 | } 314 | 315 | f_chdir :: proc "c" (L: ^lua.State) -> i32 { 316 | context = runtime.default_context() 317 | path: cstring = lua.L_checkstring(L, 1) 318 | err := os.set_current_directory(string(path)) 319 | if err != nil { 320 | lua.L_error(L, "chdir() failed") 321 | } 322 | return 0 323 | } 324 | 325 | f_list_dir :: proc "c" (L: ^lua.State) -> i32 { 326 | context = runtime.default_context() 327 | // create a separate temp region 328 | runtime.DEFAULT_TEMP_ALLOCATOR_TEMP_GUARD() 329 | path: cstring = lua.L_checkstring(L, 1) 330 | 331 | handle, err1 := os.open(string(path)) 332 | defer os.close(handle) 333 | if err1 != nil { 334 | lua.pushnil(L) 335 | lua.pushstring(L, strings.unsafe_string_to_cstring(os.error_string(err1))) 336 | return 2 337 | } 338 | 339 | entries, err2 := os.read_dir(handle, -1, context.temp_allocator) 340 | if err2 != nil { 341 | lua.pushnil(L) 342 | lua.pushstring(L, strings.unsafe_string_to_cstring(os.error_string(err2))) 343 | return 2 344 | } 345 | 346 | lua.newtable(L) 347 | for e, idx in entries { 348 | if e.name == "." || e.name == ".." { 349 | continue 350 | } 351 | lua.pushstring(L, cstring(strings.clone_to_cstring(e.name, context.temp_allocator))) 352 | lua.rawseti(L, -2, cast(lua.Integer)(idx + 1)) 353 | } 354 | return 1 355 | } 356 | 357 | f_absolute_path :: proc "c" (L: ^lua.State) -> i32 { 358 | context = runtime.default_context() 359 | path: cstring = lua.L_checkstring(L, 1) 360 | res, err := filepath.abs(string(path)) 361 | if !err do return 0 362 | lua.pushstring(L, strings.unsafe_string_to_cstring(res)) 363 | delete(res) 364 | return 1 365 | } 366 | 367 | f_get_file_info :: proc "c" (L: ^lua.State) -> i32 { 368 | context = runtime.default_context() 369 | path: cstring = lua.L_checkstring(L, 1) 370 | 371 | fi, err := os.stat(string(path)) 372 | defer os.file_info_delete(fi) 373 | if err != nil { 374 | lua.pushnil(L) 375 | lua.pushstring(L, strings.unsafe_string_to_cstring(os.error_string(err))) 376 | return 2 377 | } 378 | 379 | lua.createtable(L, 0, 3) 380 | lua.pushinteger(L, cast(lua.Integer)time.time_to_unix(fi.modification_time)) 381 | lua.setfield(L, -2, "modified") 382 | 383 | lua.pushinteger(L, cast(lua.Integer)fi.size) 384 | lua.setfield(L, -2, "size") 385 | 386 | if fi.is_dir { 387 | lua.pushstring(L, "dir") 388 | } else { 389 | lua.pushstring(L, "file") 390 | } 391 | lua.setfield(L, -2, "type") 392 | return 1 393 | } 394 | 395 | f_get_clipboard :: proc "c" (L: ^lua.State) -> i32 { 396 | text: cstring = sdl.GetClipboardText() 397 | if (text == nil) {return 0} 398 | lua.pushstring(L, text) 399 | sdl.free(cast([^]u8)text) 400 | return 1 401 | } 402 | 403 | f_set_clipboard :: proc "c" (L: ^lua.State) -> i32 { 404 | text: cstring = lua.L_checkstring(L, 1) 405 | sdl.SetClipboardText(text) 406 | return 0 407 | } 408 | 409 | f_get_time :: proc "c" (L: ^lua.State) -> i32 { 410 | n := cast(f64)sdl.GetPerformanceCounter() / cast(f64)sdl.GetPerformanceFrequency() 411 | lua.pushnumber(L, cast(lua.Number)n) 412 | return 1 413 | } 414 | 415 | f_sleep :: proc "c" (L: ^lua.State) -> i32 { 416 | n := lua.L_checknumber(L, 1) 417 | sdl.Delay(u32(n * 1000)) 418 | return 0 419 | } 420 | 421 | f_exec :: proc "c" (L: ^lua.State) -> i32 { 422 | context = runtime.default_context() 423 | len: libc.size_t 424 | cmd: cstring = lua.L_checkstring(L, 1, &len) 425 | buf := make([]u8, len + 32) 426 | defer delete(buf) 427 | 428 | when ODIN_OS == .Windows { 429 | _ = cmd 430 | // sprintf(buf, "cmd /c \"%s\"", cmd); 431 | // WinExec(buf, SW_HIDE); 432 | } else { 433 | fmt.bprintf(buf, "%s &", cmd) 434 | fmt.println("sasdasd ", buf) 435 | _ = libc.system(cast(cstring)raw_data(buf)) 436 | } 437 | return 0 438 | } 439 | 440 | f_fuzzy_match :: proc "c" (L: ^lua.State) -> i32 { 441 | strng := lua.L_checkstring(L, 1) 442 | pattern := lua.L_checkstring(L, 2) 443 | score: i32 = 0 444 | run: i32 = 0 445 | 446 | str := cast([^]u8)strng 447 | ptn := cast([^]u8)pattern 448 | 449 | pattern_len := len(pattern) 450 | str_len := len(strng) 451 | 452 | i, j := 0, 0 453 | for i < str_len && j < pattern_len { 454 | for str[i] == ' ' do i += 1 455 | for ptn[j] == ' ' do j += 1 456 | if (i >= str_len || j >= pattern_len) do break 457 | 458 | s := str[i] 459 | p := ptn[j] 460 | if libc.tolower(i32(s)) == libc.tolower(i32(p)) { 461 | score += run * 10 - i32(s != p) 462 | run += 1 463 | j += 1 464 | } else { 465 | score -= 10 466 | run = 0 467 | } 468 | i += 1 469 | } 470 | 471 | if j < pattern_len { 472 | return 0 473 | } 474 | 475 | remaining := str_len - i 476 | lua.pushinteger(L, cast(lua.Integer)(score - i32(remaining))) 477 | 478 | return 1 479 | } 480 | 481 | // odinfmt: disable 482 | @(private="file") 483 | lib := []lua.L_Reg { 484 | { "poll_event", f_poll_event }, 485 | { "wait_event", f_wait_event }, 486 | { "set_cursor", f_set_cursor }, 487 | { "set_window_title", f_set_window_title }, 488 | { "set_window_mode", f_set_window_mode }, 489 | { "window_has_focus", f_window_has_focus }, 490 | { "show_confirm_dialog", f_show_confirm_dialog }, 491 | { "chdir", f_chdir }, 492 | { "list_dir", f_list_dir }, 493 | { "absolute_path", f_absolute_path }, 494 | { "get_file_info", f_get_file_info }, 495 | { "get_clipboard", f_get_clipboard }, 496 | { "set_clipboard", f_set_clipboard }, 497 | { "get_time", f_get_time }, 498 | { "sleep", f_sleep }, 499 | { "exec", f_exec }, 500 | { "fuzzy_match", f_fuzzy_match }, 501 | { "search_file_find", search_file_find }, 502 | { nil, nil }, 503 | } 504 | // odinfmt: enable 505 | 506 | luaopen_system :: proc "c" (L: ^lua.State) -> i32 { 507 | context = runtime.default_context() 508 | lua.L_newlib(L, lib) 509 | return 1 510 | } 511 | 512 | --------------------------------------------------------------------------------