Merge lines.love
This commit is contained in:
commit
1fc11feb28
100
app.lua
100
app.lua
|
@ -97,6 +97,9 @@ function App.initialize_for_test()
|
||||||
App.screen.init{width=100, height=50}
|
App.screen.init{width=100, height=50}
|
||||||
App.screen.contents = {} -- clear screen
|
App.screen.contents = {} -- clear screen
|
||||||
App.filesystem = {}
|
App.filesystem = {}
|
||||||
|
App.source_dir = ''
|
||||||
|
App.current_dir = ''
|
||||||
|
App.save_dir = ''
|
||||||
App.fake_keys_pressed = {}
|
App.fake_keys_pressed = {}
|
||||||
App.fake_mouse_state = {x=-1, y=-1}
|
App.fake_mouse_state = {x=-1, y=-1}
|
||||||
App.initialize_globals()
|
App.initialize_globals()
|
||||||
|
@ -169,12 +172,12 @@ function App.screen.check(y, expected_contents, msg)
|
||||||
check_eq(contents, expected_contents, msg)
|
check_eq(contents, expected_contents, msg)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- If you access the time using App.getTime instead of love.timer.getTime,
|
-- If you access the time using App.get_time instead of love.timer.getTime,
|
||||||
-- tests will be able to move the time back and forwards as needed using
|
-- tests will be able to move the time back and forwards as needed using
|
||||||
-- App.wait_fake_time below.
|
-- App.wait_fake_time below.
|
||||||
|
|
||||||
App.time = 1
|
App.time = 1
|
||||||
function App.getTime()
|
function App.get_time()
|
||||||
return App.time
|
return App.time
|
||||||
end
|
end
|
||||||
function App.wait_fake_time(t)
|
function App.wait_fake_time(t)
|
||||||
|
@ -185,16 +188,16 @@ function App.width(text)
|
||||||
return love.graphics.getFont():getWidth(text)
|
return love.graphics.getFont():getWidth(text)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- If you access the clipboard using App.getClipboardText and
|
-- If you access the clipboard using App.get_clipboard and App.set_clipboard
|
||||||
-- App.setClipboardText instead of love.system.getClipboardText and
|
-- instead of love.system.getClipboardText and love.system.setClipboardText
|
||||||
-- love.system.setClipboardText respectively, tests will be able to manipulate
|
-- respectively, tests will be able to manipulate the clipboard by
|
||||||
-- the clipboard by reading/writing App.clipboard.
|
-- reading/writing App.clipboard.
|
||||||
|
|
||||||
App.clipboard = ''
|
App.clipboard = ''
|
||||||
function App.getClipboardText()
|
function App.get_clipboard()
|
||||||
return App.clipboard
|
return App.clipboard
|
||||||
end
|
end
|
||||||
function App.setClipboardText(s)
|
function App.set_clipboard(s)
|
||||||
App.clipboard = s
|
App.clipboard = s
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -257,26 +260,13 @@ end
|
||||||
|
|
||||||
function App.open_for_writing(filename)
|
function App.open_for_writing(filename)
|
||||||
App.filesystem[filename] = ''
|
App.filesystem[filename] = ''
|
||||||
if Current_app == nil or Current_app == 'run' then
|
return {
|
||||||
return {
|
write = function(self, s)
|
||||||
write = function(self, ...)
|
App.filesystem[filename] = App.filesystem[filename]..s
|
||||||
local args = {...}
|
end,
|
||||||
for i,s in ipairs(args) do
|
close = function(self)
|
||||||
App.filesystem[filename] = App.filesystem[filename]..s
|
end,
|
||||||
end
|
}
|
||||||
end,
|
|
||||||
close = function(self)
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
elseif Current_app == 'source' then
|
|
||||||
return {
|
|
||||||
write = function(self, s)
|
|
||||||
App.filesystem[filename] = App.filesystem[filename]..s
|
|
||||||
end,
|
|
||||||
close = function(self)
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function App.open_for_reading(filename)
|
function App.open_for_reading(filename)
|
||||||
|
@ -356,6 +346,8 @@ function prepend_debug_info_to_test_failure(test_name, err)
|
||||||
table.insert(Test_errors, full_error)
|
table.insert(Test_errors, full_error)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
nativefs = require 'nativefs'
|
||||||
|
|
||||||
-- call this once all tests are run
|
-- call this once all tests are run
|
||||||
-- can't run any tests after this
|
-- can't run any tests after this
|
||||||
function App.disable_tests()
|
function App.disable_tests()
|
||||||
|
@ -391,35 +383,33 @@ function App.disable_tests()
|
||||||
App.screen.move = love.window.setPosition
|
App.screen.move = love.window.setPosition
|
||||||
App.screen.position = love.window.getPosition
|
App.screen.position = love.window.getPosition
|
||||||
App.screen.print = love.graphics.print
|
App.screen.print = love.graphics.print
|
||||||
if Current_app == nil or Current_app == 'run' then
|
App.open_for_reading =
|
||||||
App.open_for_reading = function(filename) return io.open(filename, 'r') end
|
function(filename)
|
||||||
App.open_for_writing = function(filename) return io.open(filename, 'w') end
|
local result = nativefs.newFile(filename)
|
||||||
elseif Current_app == 'source' then
|
local ok, err = result:open('r')
|
||||||
-- HACK: source editor requires a couple of different foundational definitions
|
if ok then
|
||||||
App.open_for_reading =
|
return result
|
||||||
function(filename)
|
else
|
||||||
local result = love.filesystem.newFile(filename)
|
return ok, err
|
||||||
local ok, err = result:open('r')
|
|
||||||
if ok then
|
|
||||||
return result
|
|
||||||
else
|
|
||||||
return ok, err
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
App.open_for_writing =
|
end
|
||||||
function(filename)
|
App.open_for_writing =
|
||||||
local result = love.filesystem.newFile(filename)
|
function(filename)
|
||||||
local ok, err = result:open('w')
|
local result = nativefs.newFile(filename)
|
||||||
if ok then
|
local ok, err = result:open('w')
|
||||||
return result
|
if ok then
|
||||||
else
|
return result
|
||||||
return ok, err
|
else
|
||||||
end
|
return ok, err
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
App.getTime = love.timer.getTime
|
App.files = nativefs.getDirectoryItems
|
||||||
App.getClipboardText = love.system.getClipboardText
|
App.source_dir = love.filesystem.getSource()..'/'
|
||||||
App.setClipboardText = love.system.setClipboardText
|
App.current_dir = nativefs.getWorkingDirectory()..'/'
|
||||||
|
App.save_dir = love.filesystem.getSaveDirectory()..'/'
|
||||||
|
App.get_time = love.timer.getTime
|
||||||
|
App.get_clipboard = love.system.getClipboardText
|
||||||
|
App.set_clipboard = love.system.setClipboardText
|
||||||
App.key_down = love.keyboard.isDown
|
App.key_down = love.keyboard.isDown
|
||||||
App.mouse_move = love.mouse.setPosition
|
App.mouse_move = love.mouse.setPosition
|
||||||
App.mouse_down = love.mouse.isDown
|
App.mouse_down = love.mouse.isDown
|
||||||
|
|
6
edit.lua
6
edit.lua
|
@ -339,13 +339,13 @@ function edit.keychord_press(State, chord, key)
|
||||||
elseif chord == 'C-c' then
|
elseif chord == 'C-c' then
|
||||||
local s = Text.selection(State)
|
local s = Text.selection(State)
|
||||||
if s then
|
if s then
|
||||||
App.setClipboardText(s)
|
App.set_clipboard(s)
|
||||||
end
|
end
|
||||||
elseif chord == 'C-x' then
|
elseif chord == 'C-x' then
|
||||||
for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
|
for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
|
||||||
local s = Text.cut_selection(State, State.left, State.right)
|
local s = Text.cut_selection(State, State.left, State.right)
|
||||||
if s then
|
if s then
|
||||||
App.setClipboardText(s)
|
App.set_clipboard(s)
|
||||||
end
|
end
|
||||||
schedule_save(State)
|
schedule_save(State)
|
||||||
elseif chord == 'C-v' then
|
elseif chord == 'C-v' then
|
||||||
|
@ -354,7 +354,7 @@ function edit.keychord_press(State, chord, key)
|
||||||
-- and sometimes scroll when we didn't quite need to.
|
-- and sometimes scroll when we didn't quite need to.
|
||||||
local before_line = State.cursor1.line
|
local before_line = State.cursor1.line
|
||||||
local before = snapshot(State, before_line)
|
local before = snapshot(State, before_line)
|
||||||
local clipboard_data = App.getClipboardText()
|
local clipboard_data = App.get_clipboard()
|
||||||
for _,code in utf8.codes(clipboard_data) do
|
for _,code in utf8.codes(clipboard_data) do
|
||||||
local c = utf8.char(code)
|
local c = utf8.char(code)
|
||||||
if c == '\n' then
|
if c == '\n' then
|
||||||
|
|
|
@ -0,0 +1,496 @@
|
||||||
|
--[[
|
||||||
|
Copyright 2020 megagrump@pm.me
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
]]--
|
||||||
|
|
||||||
|
local ffi, bit = require('ffi'), require('bit')
|
||||||
|
local C = ffi.C
|
||||||
|
|
||||||
|
local File = {
|
||||||
|
getBuffer = function(self) return self._bufferMode, self._bufferSize end,
|
||||||
|
getFilename = function(self) return self._name end,
|
||||||
|
getMode = function(self) return self._mode end,
|
||||||
|
isOpen = function(self) return self._mode ~= 'c' and self._handle ~= nil end,
|
||||||
|
}
|
||||||
|
|
||||||
|
local fopen, getcwd, chdir, unlink, mkdir, rmdir
|
||||||
|
local BUFFERMODE, MODEMAP
|
||||||
|
local ByteArray = ffi.typeof('unsigned char[?]')
|
||||||
|
local function _ptr(p) return p ~= nil and p or nil end -- NULL pointer to nil
|
||||||
|
|
||||||
|
function File:open(mode)
|
||||||
|
if self._mode ~= 'c' then return false, "File " .. self._name .. " is already open" end
|
||||||
|
if not MODEMAP[mode] then return false, "Invalid open mode for " .. self._name .. ": " .. mode end
|
||||||
|
|
||||||
|
local handle = _ptr(fopen(self._name, MODEMAP[mode]))
|
||||||
|
if not handle then return false, "Could not open " .. self._name .. " in mode " .. mode end
|
||||||
|
|
||||||
|
self._handle, self._mode = ffi.gc(handle, C.fclose), mode
|
||||||
|
self:setBuffer(self._bufferMode, self._bufferSize)
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function File:close()
|
||||||
|
if self._mode == 'c' then return false, "File is not open" end
|
||||||
|
C.fclose(ffi.gc(self._handle, nil))
|
||||||
|
self._handle, self._mode = nil, 'c'
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function File:setBuffer(mode, size)
|
||||||
|
local bufferMode = BUFFERMODE[mode]
|
||||||
|
if not bufferMode then
|
||||||
|
return false, "Invalid buffer mode " .. mode .. " (expected 'none', 'full', or 'line')"
|
||||||
|
end
|
||||||
|
|
||||||
|
if mode == 'none' then
|
||||||
|
size = math.max(0, size or 0)
|
||||||
|
else
|
||||||
|
size = math.max(2, size or 2) -- Windows requires buffer to be at least 2 bytes
|
||||||
|
end
|
||||||
|
|
||||||
|
local success = self._mode == 'c' or C.setvbuf(self._handle, nil, bufferMode, size) == 0
|
||||||
|
if not success then
|
||||||
|
self._bufferMode, self._bufferSize = 'none', 0
|
||||||
|
return false, "Could not set buffer mode"
|
||||||
|
end
|
||||||
|
|
||||||
|
self._bufferMode, self._bufferSize = mode, size
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function File:getSize()
|
||||||
|
-- NOTE: The correct way to do this would be a stat() call, which requires a
|
||||||
|
-- lot more (system-specific) code. This is a shortcut that requires the file
|
||||||
|
-- to be readable.
|
||||||
|
local mustOpen = not self:isOpen()
|
||||||
|
if mustOpen and not self:open('r') then return 0 end
|
||||||
|
|
||||||
|
local pos = mustOpen and 0 or self:tell()
|
||||||
|
C.fseek(self._handle, 0, 2)
|
||||||
|
local size = self:tell()
|
||||||
|
if mustOpen then
|
||||||
|
self:close()
|
||||||
|
else
|
||||||
|
self:seek(pos)
|
||||||
|
end
|
||||||
|
return size
|
||||||
|
end
|
||||||
|
|
||||||
|
function File:read(containerOrBytes, bytes)
|
||||||
|
if self._mode ~= 'r' then return nil, 0 end
|
||||||
|
|
||||||
|
local container = bytes ~= nil and containerOrBytes or 'string'
|
||||||
|
if container ~= 'string' and container ~= 'data' then
|
||||||
|
error("Invalid container type: " .. container)
|
||||||
|
end
|
||||||
|
|
||||||
|
bytes = not bytes and containerOrBytes or 'all'
|
||||||
|
bytes = bytes == 'all' and self:getSize() - self:tell() or math.min(self:getSize() - self:tell(), bytes)
|
||||||
|
|
||||||
|
if bytes <= 0 then
|
||||||
|
local data = container == 'string' and '' or love.data.newFileData('', self._name)
|
||||||
|
return data, 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local data = love.data.newByteData(bytes)
|
||||||
|
local r = tonumber(C.fread(data:getFFIPointer(), 1, bytes, self._handle))
|
||||||
|
|
||||||
|
local str = data:getString()
|
||||||
|
data:release()
|
||||||
|
data = container == 'data' and love.filesystem.newFileData(str, self._name) or str
|
||||||
|
return data, r
|
||||||
|
end
|
||||||
|
|
||||||
|
local function lines(file, autoclose)
|
||||||
|
local BUFFERSIZE = 4096
|
||||||
|
local buffer, bufferPos = ByteArray(BUFFERSIZE), 0
|
||||||
|
local bytesRead = tonumber(C.fread(buffer, 1, BUFFERSIZE, file._handle))
|
||||||
|
|
||||||
|
local offset = file:tell()
|
||||||
|
return function()
|
||||||
|
file:seek(offset)
|
||||||
|
|
||||||
|
local line = {}
|
||||||
|
while bytesRead > 0 do
|
||||||
|
for i = bufferPos, bytesRead - 1 do
|
||||||
|
if buffer[i] == 10 then -- end of line
|
||||||
|
bufferPos = i + 1
|
||||||
|
return table.concat(line)
|
||||||
|
end
|
||||||
|
|
||||||
|
if buffer[i] ~= 13 then -- ignore CR
|
||||||
|
table.insert(line, string.char(buffer[i]))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
bytesRead = tonumber(C.fread(buffer, 1, BUFFERSIZE, file._handle))
|
||||||
|
offset, bufferPos = offset + bytesRead, 0
|
||||||
|
end
|
||||||
|
|
||||||
|
if not line[1] then
|
||||||
|
if autoclose then file:close() end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return table.concat(line)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function File:lines()
|
||||||
|
if self._mode ~= 'r' then error("File is not opened for reading") end
|
||||||
|
return lines(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
function File:write(data, size)
|
||||||
|
if self._mode ~= 'w' and self._mode ~= 'a' then
|
||||||
|
return false, "File " .. self._name .. " not opened for writing"
|
||||||
|
end
|
||||||
|
|
||||||
|
local toWrite, writeSize
|
||||||
|
if type(data) == 'string' then
|
||||||
|
writeSize = (size == nil or size == 'all') and #data or size
|
||||||
|
toWrite = data
|
||||||
|
else
|
||||||
|
writeSize = (size == nil or size == 'all') and data:getSize() or size
|
||||||
|
toWrite = data:getFFIPointer()
|
||||||
|
end
|
||||||
|
|
||||||
|
if tonumber(C.fwrite(toWrite, 1, writeSize, self._handle)) ~= writeSize then
|
||||||
|
return false, "Could not write data"
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function File:seek(pos)
|
||||||
|
return self._handle and C.fseek(self._handle, pos, 0) == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function File:tell()
|
||||||
|
if not self._handle then return nil, "Invalid position" end
|
||||||
|
return tonumber(C.ftell(self._handle))
|
||||||
|
end
|
||||||
|
|
||||||
|
function File:flush()
|
||||||
|
if self._mode ~= 'w' and self._mode ~= 'a' then
|
||||||
|
return nil, "File is not opened for writing"
|
||||||
|
end
|
||||||
|
return C.fflush(self._handle) == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function File:isEOF()
|
||||||
|
return not self:isOpen() or C.feof(self._handle) ~= 0 or self:tell() == self:getSize()
|
||||||
|
end
|
||||||
|
|
||||||
|
function File:release()
|
||||||
|
if self._mode ~= 'c' then self:close() end
|
||||||
|
self._handle = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function File:type() return 'File' end
|
||||||
|
|
||||||
|
function File:typeOf(t) return t == 'File' end
|
||||||
|
|
||||||
|
File.__index = File
|
||||||
|
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local nativefs = {}
|
||||||
|
local loveC = ffi.os == 'Windows' and ffi.load('love') or C
|
||||||
|
|
||||||
|
function nativefs.newFile(name)
|
||||||
|
if type(name) ~= 'string' then
|
||||||
|
error("bad argument #1 to 'newFile' (string expected, got " .. type(name) .. ")")
|
||||||
|
end
|
||||||
|
return setmetatable({
|
||||||
|
_name = name,
|
||||||
|
_mode = 'c',
|
||||||
|
_handle = nil,
|
||||||
|
_bufferSize = 0,
|
||||||
|
_bufferMode = 'none'
|
||||||
|
}, File)
|
||||||
|
end
|
||||||
|
|
||||||
|
function nativefs.newFileData(filepath)
|
||||||
|
local f = nativefs.newFile(filepath)
|
||||||
|
local ok, err = f:open('r')
|
||||||
|
if not ok then return nil, err end
|
||||||
|
|
||||||
|
local data, err = f:read('data', 'all')
|
||||||
|
f:close()
|
||||||
|
return data, err
|
||||||
|
end
|
||||||
|
|
||||||
|
function nativefs.mount(archive, mountPoint, appendToPath)
|
||||||
|
return loveC.PHYSFS_mount(archive, mountPoint, appendToPath and 1 or 0) ~= 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function nativefs.unmount(archive)
|
||||||
|
return loveC.PHYSFS_unmount(archive) ~= 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function nativefs.read(containerOrName, nameOrSize, sizeOrNil)
|
||||||
|
local container, name, size
|
||||||
|
if sizeOrNil then
|
||||||
|
container, name, size = containerOrName, nameOrSize, sizeOrNil
|
||||||
|
elseif not nameOrSize then
|
||||||
|
container, name, size = 'string', containerOrName, 'all'
|
||||||
|
else
|
||||||
|
if type(nameOrSize) == 'number' or nameOrSize == 'all' then
|
||||||
|
container, name, size = 'string', containerOrName, nameOrSize
|
||||||
|
else
|
||||||
|
container, name, size = containerOrName, nameOrSize, 'all'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local file = nativefs.newFile(name)
|
||||||
|
local ok, err = file:open('r')
|
||||||
|
if not ok then return nil, err end
|
||||||
|
|
||||||
|
local data, size = file:read(container, size)
|
||||||
|
file:close()
|
||||||
|
return data, size
|
||||||
|
end
|
||||||
|
|
||||||
|
local function writeFile(mode, name, data, size)
|
||||||
|
local file = nativefs.newFile(name)
|
||||||
|
local ok, err = file:open(mode)
|
||||||
|
if not ok then return nil, err end
|
||||||
|
|
||||||
|
ok, err = file:write(data, size or 'all')
|
||||||
|
file:close()
|
||||||
|
return ok, err
|
||||||
|
end
|
||||||
|
|
||||||
|
function nativefs.write(name, data, size)
|
||||||
|
return writeFile('w', name, data, size)
|
||||||
|
end
|
||||||
|
|
||||||
|
function nativefs.append(name, data, size)
|
||||||
|
return writeFile('a', name, data, size)
|
||||||
|
end
|
||||||
|
|
||||||
|
function nativefs.lines(name)
|
||||||
|
local f = nativefs.newFile(name)
|
||||||
|
local ok, err = f:open('r')
|
||||||
|
if not ok then return nil, err end
|
||||||
|
return lines(f, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
function nativefs.load(name)
|
||||||
|
local chunk, err = nativefs.read(name)
|
||||||
|
if not chunk then return nil, err end
|
||||||
|
return loadstring(chunk, name)
|
||||||
|
end
|
||||||
|
|
||||||
|
function nativefs.getWorkingDirectory()
|
||||||
|
return getcwd()
|
||||||
|
end
|
||||||
|
|
||||||
|
function nativefs.setWorkingDirectory(path)
|
||||||
|
if not chdir(path) then return false, "Could not set working directory" end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function nativefs.getDriveList()
|
||||||
|
if ffi.os ~= 'Windows' then return { '/' } end
|
||||||
|
local drives, bits = {}, C.GetLogicalDrives()
|
||||||
|
for i = 0, 25 do
|
||||||
|
if bit.band(bits, 2 ^ i) > 0 then
|
||||||
|
table.insert(drives, string.char(65 + i) .. ':/')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return drives
|
||||||
|
end
|
||||||
|
|
||||||
|
function nativefs.createDirectory(path)
|
||||||
|
local current = path:sub(1, 1) == '/' and '/' or ''
|
||||||
|
for dir in path:gmatch('[^/\\]+') do
|
||||||
|
current = current .. dir .. '/'
|
||||||
|
local info = nativefs.getInfo(current, 'directory')
|
||||||
|
if not info and not mkdir(current) then return false, "Could not create directory " .. current end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function nativefs.remove(name)
|
||||||
|
local info = nativefs.getInfo(name)
|
||||||
|
if not info then return false, "Could not remove " .. name end
|
||||||
|
if info.type == 'directory' then
|
||||||
|
if not rmdir(name) then return false, "Could not remove directory " .. name end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
if not unlink(name) then return false, "Could not remove file " .. name end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function withTempMount(dir, fn, ...)
|
||||||
|
local mountPoint = _ptr(loveC.PHYSFS_getMountPoint(dir))
|
||||||
|
if mountPoint then return fn(ffi.string(mountPoint), ...) end
|
||||||
|
if not nativefs.mount(dir, '__nativefs__temp__') then return false, "Could not mount " .. dir end
|
||||||
|
local a, b = fn('__nativefs__temp__', ...)
|
||||||
|
nativefs.unmount(dir)
|
||||||
|
return a, b
|
||||||
|
end
|
||||||
|
|
||||||
|
function nativefs.getDirectoryItems(dir)
|
||||||
|
if type(dir) ~= "string" then
|
||||||
|
error("bad argument #1 to 'getDirectoryItems' (string expected, got " .. type(dir) .. ")")
|
||||||
|
end
|
||||||
|
local result, err = withTempMount(dir, love.filesystem.getDirectoryItems)
|
||||||
|
return result or {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getDirectoryItemsInfo(path, filtertype)
|
||||||
|
local items = {}
|
||||||
|
local files = love.filesystem.getDirectoryItems(path)
|
||||||
|
for i = 1, #files do
|
||||||
|
local filepath = string.format('%s/%s', path, files[i])
|
||||||
|
local info = love.filesystem.getInfo(filepath, filtertype)
|
||||||
|
if info then
|
||||||
|
info.name = files[i]
|
||||||
|
table.insert(items, info)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return items
|
||||||
|
end
|
||||||
|
|
||||||
|
function nativefs.getDirectoryItemsInfo(path, filtertype)
|
||||||
|
if type(path) ~= "string" then
|
||||||
|
error("bad argument #1 to 'getDirectoryItemsInfo' (string expected, got " .. type(path) .. ")")
|
||||||
|
end
|
||||||
|
local result, err = withTempMount(path, getDirectoryItemsInfo, filtertype)
|
||||||
|
return result or {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getInfo(path, file, filtertype)
|
||||||
|
local filepath = string.format('%s/%s', path, file)
|
||||||
|
return love.filesystem.getInfo(filepath, filtertype)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function leaf(p)
|
||||||
|
p = p:gsub('\\', '/')
|
||||||
|
local last, a = p, 1
|
||||||
|
while a do
|
||||||
|
a = p:find('/', a + 1)
|
||||||
|
if a then
|
||||||
|
last = p:sub(a + 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return last
|
||||||
|
end
|
||||||
|
|
||||||
|
function nativefs.getInfo(path, filtertype)
|
||||||
|
if type(path) ~= 'string' then
|
||||||
|
error("bad argument #1 to 'getInfo' (string expected, got " .. type(path) .. ")")
|
||||||
|
end
|
||||||
|
local dir = path:match("(.*[\\/]).*$") or './'
|
||||||
|
local file = leaf(path)
|
||||||
|
local result, err = withTempMount(dir, getInfo, file, filtertype)
|
||||||
|
return result or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
MODEMAP = { r = 'rb', w = 'wb', a = 'ab' }
|
||||||
|
local MAX_PATH = 4096
|
||||||
|
|
||||||
|
ffi.cdef([[
|
||||||
|
int PHYSFS_mount(const char* dir, const char* mountPoint, int appendToPath);
|
||||||
|
int PHYSFS_unmount(const char* dir);
|
||||||
|
const char* PHYSFS_getMountPoint(const char* dir);
|
||||||
|
|
||||||
|
typedef struct FILE FILE;
|
||||||
|
|
||||||
|
FILE* fopen(const char* path, const char* mode);
|
||||||
|
size_t fread(void* ptr, size_t size, size_t nmemb, FILE* stream);
|
||||||
|
size_t fwrite(const void* ptr, size_t size, size_t nmemb, FILE* stream);
|
||||||
|
int fclose(FILE* stream);
|
||||||
|
int fflush(FILE* stream);
|
||||||
|
size_t fseek(FILE* stream, size_t offset, int whence);
|
||||||
|
size_t ftell(FILE* stream);
|
||||||
|
int setvbuf(FILE* stream, char* buffer, int mode, size_t size);
|
||||||
|
int feof(FILE* stream);
|
||||||
|
]])
|
||||||
|
|
||||||
|
if ffi.os == 'Windows' then
|
||||||
|
ffi.cdef([[
|
||||||
|
int MultiByteToWideChar(unsigned int cp, uint32_t flags, const char* mb, int cmb, const wchar_t* wc, int cwc);
|
||||||
|
int WideCharToMultiByte(unsigned int cp, uint32_t flags, const wchar_t* wc, int cwc, const char* mb,
|
||||||
|
int cmb, const char* def, int* used);
|
||||||
|
int GetLogicalDrives(void);
|
||||||
|
int CreateDirectoryW(const wchar_t* path, void*);
|
||||||
|
int _wchdir(const wchar_t* path);
|
||||||
|
wchar_t* _wgetcwd(wchar_t* buffer, int maxlen);
|
||||||
|
FILE* _wfopen(const wchar_t* path, const wchar_t* mode);
|
||||||
|
int _wunlink(const wchar_t* path);
|
||||||
|
int _wrmdir(const wchar_t* path);
|
||||||
|
]])
|
||||||
|
|
||||||
|
BUFFERMODE = { full = 0, line = 64, none = 4 }
|
||||||
|
|
||||||
|
local function towidestring(str)
|
||||||
|
local size = C.MultiByteToWideChar(65001, 0, str, #str, nil, 0)
|
||||||
|
local buf = ffi.new('wchar_t[?]', size + 1)
|
||||||
|
C.MultiByteToWideChar(65001, 0, str, #str, buf, size)
|
||||||
|
return buf
|
||||||
|
end
|
||||||
|
|
||||||
|
local function toutf8string(wstr)
|
||||||
|
local size = C.WideCharToMultiByte(65001, 0, wstr, -1, nil, 0, nil, nil)
|
||||||
|
local buf = ffi.new('char[?]', size + 1)
|
||||||
|
C.WideCharToMultiByte(65001, 0, wstr, -1, buf, size, nil, nil)
|
||||||
|
return ffi.string(buf)
|
||||||
|
end
|
||||||
|
|
||||||
|
local nameBuffer = ffi.new('wchar_t[?]', MAX_PATH + 1)
|
||||||
|
|
||||||
|
fopen = function(path, mode) return C._wfopen(towidestring(path), towidestring(mode)) end
|
||||||
|
getcwd = function() return toutf8string(C._wgetcwd(nameBuffer, MAX_PATH)) end
|
||||||
|
chdir = function(path) return C._wchdir(towidestring(path)) == 0 end
|
||||||
|
unlink = function(path) return C._wunlink(towidestring(path)) == 0 end
|
||||||
|
mkdir = function(path) return C.CreateDirectoryW(towidestring(path), nil) ~= 0 end
|
||||||
|
rmdir = function(path) return C._wrmdir(towidestring(path)) == 0 end
|
||||||
|
else
|
||||||
|
BUFFERMODE = { full = 0, line = 1, none = 2 }
|
||||||
|
|
||||||
|
ffi.cdef([[
|
||||||
|
char* getcwd(char *buffer, int maxlen);
|
||||||
|
int chdir(const char* path);
|
||||||
|
int unlink(const char* path);
|
||||||
|
int mkdir(const char* path, int mode);
|
||||||
|
int rmdir(const char* path);
|
||||||
|
]])
|
||||||
|
|
||||||
|
local nameBuffer = ByteArray(MAX_PATH)
|
||||||
|
|
||||||
|
fopen = C.fopen
|
||||||
|
unlink = function(path) return ffi.C.unlink(path) == 0 end
|
||||||
|
chdir = function(path) return ffi.C.chdir(path) == 0 end
|
||||||
|
mkdir = function(path) return ffi.C.mkdir(path, 0x1ed) == 0 end
|
||||||
|
rmdir = function(path) return ffi.C.rmdir(path) == 0 end
|
||||||
|
|
||||||
|
getcwd = function()
|
||||||
|
local cwd = _ptr(C.getcwd(nameBuffer, MAX_PATH))
|
||||||
|
return cwd and ffi.string(cwd) or nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return nativefs
|
14
reference.md
14
reference.md
|
@ -347,16 +347,12 @@ The following facilities help set these things up:
|
||||||
* `json.decode(obj)` -- turns a JSON string into a Lua object.
|
* `json.decode(obj)` -- turns a JSON string into a Lua object.
|
||||||
(From [json.lua](https://github.com/rxi/json.lua).)
|
(From [json.lua](https://github.com/rxi/json.lua).)
|
||||||
|
|
||||||
* `love.filesystem.getDirectoryItems(dir)` -- returns an unsorted array of the
|
* `App.files(dir)` -- returns an unsorted array of the files and directories
|
||||||
files and directories available under `dir`. `dir` must be relative to
|
available under `dir`.
|
||||||
[LÖVE's save directory](https://love2d.org/wiki/love.filesystem.getSaveDirectory).
|
|
||||||
There is no easy, portable way in Lua/LÖVE to list directories outside the
|
|
||||||
save dir.
|
|
||||||
(From [LÖVE](https://love2d.org/wiki/love.filesystem.getDirectoryItems).]
|
(From [LÖVE](https://love2d.org/wiki/love.filesystem.getDirectoryItems).]
|
||||||
|
|
||||||
* `love.filesystem.getInfo(filename)` -- returns some information about
|
* `love.filesystem.getInfo(filename)` -- returns some information about
|
||||||
`filename`, particularly whether it exists (non-`nil` return value) or not.
|
`filename`, particularly whether it exists (non-`nil` return value) or not.
|
||||||
`filename` must be relative to [LÖVE's save directory](https://love2d.org/wiki/love.filesystem.getSaveDirectory).
|
|
||||||
(From [LÖVE](https://love2d.org/wiki/love.filesystem.getInfo).]
|
(From [LÖVE](https://love2d.org/wiki/love.filesystem.getInfo).]
|
||||||
|
|
||||||
* `os.remove(filename)` -- removes a file or empty directory. Definitely make
|
* `os.remove(filename)` -- removes a file or empty directory. Definitely make
|
||||||
|
@ -368,15 +364,15 @@ and [the Lua manual](https://www.lua.org/manual/5.1/manual.html#5.7).
|
||||||
|
|
||||||
### desiderata
|
### desiderata
|
||||||
|
|
||||||
* `App.getTime()` -- returns the number of seconds elapsed since some
|
* `App.get_time()` -- returns the number of seconds elapsed since some
|
||||||
unspecified start time.
|
unspecified start time.
|
||||||
(Based on [LÖVE](https://love2d.org/wiki/love.timer.getTime).)
|
(Based on [LÖVE](https://love2d.org/wiki/love.timer.getTime).)
|
||||||
|
|
||||||
* `App.getClipboardText()` -- returns a string with the current clipboard
|
* `App.get_clipboard()` -- returns a string with the current clipboard
|
||||||
contents.
|
contents.
|
||||||
(Based on [LÖVE](https://love2d.org/wiki/love.system.getClipboardText).)
|
(Based on [LÖVE](https://love2d.org/wiki/love.system.getClipboardText).)
|
||||||
|
|
||||||
* `App.setClipboardText(text)` -- stores the string `text` in the clipboard.
|
* `App.set_clipboard(text)` -- stores the string `text` in the clipboard.
|
||||||
(Based on [LÖVE](https://love2d.org/wiki/love.system.setClipboardText).)
|
(Based on [LÖVE](https://love2d.org/wiki/love.system.setClipboardText).)
|
||||||
|
|
||||||
* `array.find(arr, elem)` -- scan table `arr` for `elem` assuming it's
|
* `array.find(arr, elem)` -- scan table `arr` for `elem` assuming it's
|
||||||
|
|
|
@ -448,13 +448,13 @@ function edit.keychord_press(State, chord, key)
|
||||||
elseif chord == 'C-c' then
|
elseif chord == 'C-c' then
|
||||||
local s = Text.selection(State)
|
local s = Text.selection(State)
|
||||||
if s then
|
if s then
|
||||||
App.setClipboardText(s)
|
App.set_clipboard(s)
|
||||||
end
|
end
|
||||||
elseif chord == 'C-x' then
|
elseif chord == 'C-x' then
|
||||||
for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
|
for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
|
||||||
local s = Text.cut_selection(State, State.left, State.right)
|
local s = Text.cut_selection(State, State.left, State.right)
|
||||||
if s then
|
if s then
|
||||||
App.setClipboardText(s)
|
App.set_clipboard(s)
|
||||||
end
|
end
|
||||||
schedule_save(State)
|
schedule_save(State)
|
||||||
elseif chord == 'C-v' then
|
elseif chord == 'C-v' then
|
||||||
|
@ -463,7 +463,7 @@ function edit.keychord_press(State, chord, key)
|
||||||
-- and sometimes scroll when we didn't quite need to.
|
-- and sometimes scroll when we didn't quite need to.
|
||||||
local before_line = State.cursor1.line
|
local before_line = State.cursor1.line
|
||||||
local before = snapshot(State, before_line)
|
local before = snapshot(State, before_line)
|
||||||
local clipboard_data = App.getClipboardText()
|
local clipboard_data = App.get_clipboard()
|
||||||
for _,code in utf8.codes(clipboard_data) do
|
for _,code in utf8.codes(clipboard_data) do
|
||||||
local c = utf8.char(code)
|
local c = utf8.char(code)
|
||||||
if c == '\n' then
|
if c == '\n' then
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
-- primitives for saving to file and loading from file
|
-- primitives for saving to file and loading from file
|
||||||
|
|
||||||
function file_exists(filename)
|
function file_exists(filename)
|
||||||
local infile = App.open_for_reading(filename)
|
local infile = App.open_for_reading(App.save_dir..filename)
|
||||||
|
if not infile then
|
||||||
|
infile = App.open_for_reading(App.source_dir..filename)
|
||||||
|
end
|
||||||
if infile then
|
if infile then
|
||||||
infile:close()
|
infile:close()
|
||||||
return true
|
return true
|
||||||
|
@ -10,8 +13,12 @@ function file_exists(filename)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- the source editor supports only files in the save dir backed by the source dir
|
||||||
function load_from_disk(State)
|
function load_from_disk(State)
|
||||||
local infile = App.open_for_reading(State.filename)
|
local infile = App.open_for_reading(App.save_dir..State.filename)
|
||||||
|
if not infile then
|
||||||
|
infile = App.open_for_reading(App.source_dir..State.filename)
|
||||||
|
end
|
||||||
State.lines = load_from_file(infile)
|
State.lines = load_from_file(infile)
|
||||||
if infile then infile:close() end
|
if infile then infile:close() end
|
||||||
end
|
end
|
||||||
|
@ -37,7 +44,7 @@ function load_from_file(infile)
|
||||||
end
|
end
|
||||||
|
|
||||||
function save_to_disk(State)
|
function save_to_disk(State)
|
||||||
local outfile = App.open_for_writing(State.filename)
|
local outfile = App.open_for_writing(App.save_dir..State.filename)
|
||||||
if outfile == nil then
|
if outfile == nil then
|
||||||
error('failed to write to "'..State.filename..'"')
|
error('failed to write to "'..State.filename..'"')
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue