<commit_before>require "fritomod/Strings";
require "fritomod/Lists";
require "fritomod/Frames-Position";
require "fritomod/Mapper";
require "fritomod/UI-List";
require "rainback/Editor";
require "hack/Toolbox";

local editor = UI.Editor:New(UIParent);
local handle = editor:Handle();
Frames.Position(handle, "EditorFrame", Anchors.Center);
Frames.Draggable(handle);

local toolbox = Hack.Toolbox:New();
local pages = {};

function PageForName(name)
    for i=1, #pages do
        local page = pages[i];
        if page:GetName() == name then
            return page;
        end;
    end;
end;

function PageForScript(script)
    for i=1, #pages do
        local page = pages[i];
        if page:GetScript() == script then
            return page;
        end;
    end;
end;

Events.CLOSE(function()
    Lists.Each(pages, "Destroy");
end);

toolbox:OnUpdate(function()
    if editor:GetPage() ~= nil then
        return;
    end;
    local first = next(toolbox:GetScripts());
    if first == nil then
        return;
    end;
    editor:SetPage(PageForScript(first));
end);

local function Parser(page, command)
    local script = page:GetScript();

    local match, name = command:grep([[^use\s+(.+)$]]);
    if match then
        return script:AddConnector(Hack.Connectors.Use(function(env, dtor)
            local usedScript = toolbox:GetScript(name);
            assert(usedScript, "No script was available with name '" .. tostring(name) .. "'");
            dtor(usedScript:OnUpdate(script, "FireUpdate"));

            -- Pass env twice, once for the environment where the script
            -- is ran, and again to provide it as a parameter to the invoked script.
            return usedScript:Execute(env, env, dtor);
        end));
    end;
    local match, assetName, globalName = command:grep(
        [[^asset\s+([^\s]+)\s+([^\s]+)$]]
    );
    if match then
        return script:AddConnector(function(env)
            local assetScript = toolbox:GetScript(assetName);
            assert(assetScript, "No asset was available with name '" .. tostring(assetName) .. "'");
            env:AddDestructor(assetScript:OnUpdate(script, "FireUpdate"));
            return env:Change(globalName, assetScript:Execute(env));
        end);
    end;
    local match, name, default = command:grep(
        [[^param\s+([^\s]+)(?:\s*=\s*([^\s].*))?$]]
    );
    if match then
        print(name, default);
        return Noop;
    end;
    local match = command:grep(
        [[^autorun$]]
    );
    if match then
        return script:AddConnector(function(env)
            env:AddDestructor(page:OnUpdate(function()
                local success, msg = xpcall(Curry(page.Run, page), debug.traceback);
                if not success then
                    print(msg);
                end;
            end));
        end);
    end;
end;

local function NewScript(content)
    local script = Hack.Script:New();
    script:AddConnector("Set", "require", require);
    script:AddConnector(Hack.Connectors.Metadata(function(metadata)
        if not metadata.name then
            metadata.name = toolbox:NameFor(script);
        end;
    end));
    script:AddConnector(Hack.Connectors.Global("Undoer", Hack.Assets.Undoer()));
    script:SetContent(content);

    return script;
end;

local function NewPage(name, content)
    local script = NewScript(content);

    local page = Hack.ScriptPage:New(script);
    page:SetName(name);
    page:SetCommandParser(Parser);

    toolbox:AddScript(name, script);
    table.insert(pages, page);

    return page;
end;

local listHeader = Frames.New();
Frames.WH(listHeader, 100, 20);
Frames.Color(listHeader, "grey");
Anchors.HFlip(listHeader, handle, "topleft", -1);

local listBG = Frames.New();
Frames.Width(listBG, 100);
Frames.Color(listBG, "white", .5);
Anchors.VFlip(listBG, listHeader, "bottomleft", -1);
Anchors.HFlip(listBG, editor.editorFrame, "bottomleft", -1);

local mapper = Mapper:New();

function mapper:CanReuseContent(text, page)
    text:SetText(page:GetName());
    return true;
end;

mapper:SetMapper(function(page)
    local text = Frames.Text(UIParent, "default", 10);
    text:SetText(page:GetName());
    Callbacks.Click(text, function()
        editor:SetPage(page);
    end);
    return text;
end);

mapper:AddSourceList(pages);

local frames = {};
mapper:AddDestination(frames);

mapper:AddDestination(function(name, frame)
    if frame == nil then
        Frames.Destroy(frames[name]);
    end;
end);
toolbox:OnUpdate(mapper, "Update");

local list = UI.List:New();
list:SetLayout(Anchors.VJustify, "topleft");
list:SetContent(frames);
list:OnUpdate(function(ref)
    if not ref then
        return;
    end;
    Anchors.Share(ref, listBG, "topleft", 3);
end);
mapper:OnUpdate(list, "Update");

Callbacks.PersistentValue("Toolbox", function(pagesData)
    pagesData = pagesData or {};
    Lists.Each(pagesData, function(pageData)
        local page = NewPage(pageData.name, pageData.text);

        if pageData.commands then
            Lists.Each(pageData.commands, page, "AddCommand");
        end;
    end);
    mapper:Update();
    return function()
        return Lists.Map(pages, function(page)
            return {
                name = page:GetName(),
                text = page:GetContent(),
                commands = page:GetCommands(),
            };
        end);
    end;
end);

Callbacks.PersistentValue("Toolbox-Selection", function(selection)
    if selection ~= nil then
        local page = PageForName(selection);
        if page then
            editor:SetPage(page);
        end;
    end;
    return function()
        local page = editor:GetPage();
        if page then
            return page:GetName();
        end;
    end;
end);

function Slash.scr(cmd)
    local cmd, name = unpack(Strings.Split(" ", cmd, 2));
    if cmd == "new" or cmd == "add" then
        local page = NewPage(name);
        editor:SetPage(page);
        mapper:Update();
    elseif cmd == "rename" or cmd == "mv" then
        local old, new = unpack(Strings.Split(" ", name));
        if not old or not new then
            return;
        end;
        if old == new then
            -- Nothing to do, so just return
            return;
        end;
        if PageForName(new) then
            print("Refusing to overwrite a name in use: " .. tostring(new));
            return;
        end;
        PageForName(old):SetName(new);
        toolbox:RenameScript(old, new);
        mapper:Update();
    elseif cmd == "del" or cmd == "rm" then
        local page = PageForName(name);
        if not page then
            return;
        end;
        if editor:GetPage() == page then
            -- We're currently viewing this page, so try to find another page to look at
            if #pages == 1 then
                -- No pages left, so just view nothing
                editor:Reset();
            else
                local i = Lists.KeyFor(pages, page);
                if i == #pages then
                    editor:SetPage(pages[i - 1]);
                else
                    editor:SetPage(pages[i + 1]);
                end;
            end;
        end;
        Lists.Remove(pages, page);
        toolbox:RemoveScript(name);
        mapper:Update();
    end;
end;
<commit_msg>Don't reset the environment if the script fails to compile<commit_after>require "fritomod/Strings";
require "fritomod/Lists";
require "fritomod/Frames-Position";
require "fritomod/Mapper";
require "fritomod/UI-List";
require "rainback/Editor";
require "hack/Toolbox";

local editor = UI.Editor:New(UIParent);
local handle = editor:Handle();
Frames.Position(handle, "EditorFrame", Anchors.Center);
Frames.Draggable(handle);

local toolbox = Hack.Toolbox:New();
local pages = {};

function PageForName(name)
    for i=1, #pages do
        local page = pages[i];
        if page:GetName() == name then
            return page;
        end;
    end;
end;

function PageForScript(script)
    for i=1, #pages do
        local page = pages[i];
        if page:GetScript() == script then
            return page;
        end;
    end;
end;

Events.CLOSE(function()
    Lists.Each(pages, "Destroy");
end);

toolbox:OnUpdate(function()
    if editor:GetPage() ~= nil then
        return;
    end;
    local first = next(toolbox:GetScripts());
    if first == nil then
        return;
    end;
    editor:SetPage(PageForScript(first));
end);

local function Parser(page, command)
    local script = page:GetScript();

    local match, name = command:grep([[^use\s+(.+)$]]);
    if match then
        return script:AddConnector(Hack.Connectors.Use(function(env, dtor)
            local usedScript = toolbox:GetScript(name);
            assert(usedScript, "No script was available with name '" .. tostring(name) .. "'");
            dtor(usedScript:OnUpdate(script, "FireUpdate"));

            -- Pass env twice, once for the environment where the script
            -- is ran, and again to provide it as a parameter to the invoked script.
            return usedScript:Execute(env, env, dtor);
        end));
    end;
    local match, assetName, globalName = command:grep(
        [[^asset\s+([^\s]+)\s+([^\s]+)$]]
    );
    if match then
        return script:AddConnector(function(env)
            local assetScript = toolbox:GetScript(assetName);
            assert(assetScript, "No asset was available with name '" .. tostring(assetName) .. "'");
            env:AddDestructor(assetScript:OnUpdate(script, "FireUpdate"));
            return env:Change(globalName, assetScript:Execute(env));
        end);
    end;
    local match, name, default = command:grep(
        [[^param\s+([^\s]+)(?:\s*=\s*([^\s].*))?$]]
    );
    if match then
        print(name, default);
        return Noop;
    end;
    local match = command:grep(
        [[^autorun$]]
    );
    if match then
        return script:AddConnector(function(env)
            env:AddDestructor(page:OnUpdate(function()
                if not page:GetScript():Compiles() then
                    return;
                end;
                local success, msg = xpcall(Curry(page.Run, page), debug.traceback);
                if not success then
                    print(msg);
                end;
            end));
        end);
    end;
end;

local function NewScript(content)
    local script = Hack.Script:New();
    script:AddConnector("Set", "require", require);
    script:AddConnector(Hack.Connectors.Metadata(function(metadata)
        if not metadata.name then
            metadata.name = toolbox:NameFor(script);
        end;
    end));
    script:AddConnector(Hack.Connectors.Global("Undoer", Hack.Assets.Undoer()));
    script:SetContent(content);

    return script;
end;

local function NewPage(name, content)
    local script = NewScript(content);

    local page = Hack.ScriptPage:New(script);
    page:SetName(name);
    page:SetCommandParser(Parser);

    toolbox:AddScript(name, script);
    table.insert(pages, page);

    return page;
end;

local listHeader = Frames.New();
Frames.WH(listHeader, 100, 20);
Frames.Color(listHeader, "grey");
Anchors.HFlip(listHeader, handle, "topleft", -1);

local listBG = Frames.New();
Frames.Width(listBG, 100);
Frames.Color(listBG, "white", .5);
Anchors.VFlip(listBG, listHeader, "bottomleft", -1);
Anchors.HFlip(listBG, editor.editorFrame, "bottomleft", -1);

local mapper = Mapper:New();

function mapper:CanReuseContent(text, page)
    text:SetText(page:GetName());
    return true;
end;

mapper:SetMapper(function(page)
    local text = Frames.Text(UIParent, "default", 10);
    text:SetText(page:GetName());
    Callbacks.Click(text, function()
        editor:SetPage(page);
    end);
    return text;
end);

mapper:AddSourceList(pages);

local frames = {};
mapper:AddDestination(frames);

mapper:AddDestination(function(name, frame)
    if frame == nil then
        Frames.Destroy(frames[name]);
    end;
end);
toolbox:OnUpdate(mapper, "Update");

local list = UI.List:New();
list:SetLayout(Anchors.VJustify, "topleft");
list:SetContent(frames);
list:OnUpdate(function(ref)
    if not ref then
        return;
    end;
    Anchors.Share(ref, listBG, "topleft", 3);
end);
mapper:OnUpdate(list, "Update");

Callbacks.PersistentValue("Toolbox", function(pagesData)
    pagesData = pagesData or {};
    Lists.Each(pagesData, function(pageData)
        local page = NewPage(pageData.name, pageData.text);

        if pageData.commands then
            Lists.Each(pageData.commands, page, "AddCommand");
        end;
    end);
    mapper:Update();
    return function()
        return Lists.Map(pages, function(page)
            return {
                name = page:GetName(),
                text = page:GetContent(),
                commands = page:GetCommands(),
            };
        end);
    end;
end);

Callbacks.PersistentValue("Toolbox-Selection", function(selection)
    if selection ~= nil then
        local page = PageForName(selection);
        if page then
            editor:SetPage(page);
        end;
    end;
    return function()
        local page = editor:GetPage();
        if page then
            return page:GetName();
        end;
    end;
end);

function Slash.scr(cmd)
    local cmd, name = unpack(Strings.Split(" ", cmd, 2));
    if cmd == "new" or cmd == "add" then
        local page = NewPage(name);
        editor:SetPage(page);
        mapper:Update();
    elseif cmd == "rename" or cmd == "mv" then
        local old, new = unpack(Strings.Split(" ", name));
        if not old or not new then
            return;
        end;
        if old == new then
            -- Nothing to do, so just return
            return;
        end;
        if PageForName(new) then
            print("Refusing to overwrite a name in use: " .. tostring(new));
            return;
        end;
        PageForName(old):SetName(new);
        toolbox:RenameScript(old, new);
        mapper:Update();
    elseif cmd == "del" or cmd == "rm" then
        local page = PageForName(name);
        if not page then
            return;
        end;
        if editor:GetPage() == page then
            -- We're currently viewing this page, so try to find another page to look at
            if #pages == 1 then
                -- No pages left, so just view nothing
                editor:Reset();
            else
                local i = Lists.KeyFor(pages, page);
                if i == #pages then
                    editor:SetPage(pages[i - 1]);
                else
                    editor:SetPage(pages[i + 1]);
                end;
            end;
        end;
        Lists.Remove(pages, page);
        toolbox:RemoveScript(name);
        mapper:Update();
    end;
end;

{'fraction_non_alphanumeric': 0.13028361058085627, 'fraction_numerical': 0.002863376056722116, 'mean_word_length': 2.4564090480678606, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4848902', 'n_tokens_mistral': 4496, 'n_tokens_neox': 4385, 'n_words': 1111}