xeluxee / competitest.nvim

CompetiTest.nvim is a Neovim plugin for Competitive Programming: it can manage and check testcases, download problems and contests from online judges and much more
GNU Lesser General Public License v3.0
417 stars 20 forks source link

[#feature] diff view between the actual and the expected output #10

Closed MuhammadSawalhy closed 1 year ago

MuhammadSawalhy commented 2 years ago

Sometimes there is a difference between the actual output and the expected one. So it is a good idea to enable a diff view with a keybinding or make it the default if a wrong answer is encountered.

We can do it with our terminals but the speed is the goal behind the plugin, we need to do it with just a key press.

Examples:

shadmansaleh commented 1 year ago

This would be a nice addition. Even just showing vim.diff() output in a buffer marked with diff filetype would be great.

This should show diff output in a floating window.

diff --git a/lua/competitest/init.lua b/lua/competitest/init.lua
index a0eebdc..b8b5474 100644
--- a/lua/competitest/init.lua
+++ b/lua/competitest/init.lua
@@ -46,6 +46,7 @@ local default_config = {
            view_output = { "a", "A" },
            view_stdout = { "o", "O" },
            view_stderr = { "e", "E" },
+           view_diff = { "D", "<c-d>" },
            close = { "q", "Q" },
        },
        viewer = { -- viewer window, to view in detail a stream (input, expected output, stdout or stderr)
diff --git a/lua/competitest/runner.lua b/lua/competitest/runner.lua
index 4212813..094b8cd 100644
--- a/lua/competitest/runner.lua
+++ b/lua/competitest/runner.lua
@@ -74,6 +74,7 @@ function TCRunner:run_testcases(tctbl, compile)
                stdin = tc.input,
                -- expout = expected output
                expout = tc.output,
+               diff = '',
                tcnum = tcnum,
                timelimit = self.config.maximum_time,
            })
@@ -230,6 +231,7 @@ function TCRunner:execute_testcase(tcindex, exec, args, dir, callback)
            self:update_ui(true)
        else
            tc.stdout = tc.stdout .. string.gsub(data, "\r\n", "\n")
+           tc.diff = vim.diff(tc.expout, tc.stdout)
            self:update_ui()
        end
    end)
diff --git a/lua/competitest/runner_ui/init.lua b/lua/competitest/runner_ui/init.lua
index f4e1439..8526f04 100644
--- a/lua/competitest/runner_ui/init.lua
+++ b/lua/competitest/runner_ui/init.lua
@@ -29,6 +29,7 @@ function RunnerUI:new(interface, restore_winid)
            eo = nil, -- expected output
            tc = nil, -- testcases selector
            vw = nil, -- viewer popup
+           di = nil, -- diff viewer
        },
        tcdata = nil, -- table containing testcases data and results
    }
@@ -73,6 +74,7 @@ function RunnerUI:show_ui()
            se = "Errors", -- standard error
            eo = "Expected Output", -- expected output
            tc = "Testcases", -- testcases selector
+           di = "Diff", -- Diff
        }
        for n, w in pairs(self.windows) do
            if n ~= "vw" then
@@ -170,6 +172,10 @@ function RunnerUI:show_ui()
        for _, map in ipairs(self.runner.config.runner_ui.mappings.view_stderr) do
            open_viewer(map, "se")
        end
+       -- view diff in a bigger window keymaps
+       for _, map in ipairs(self.runner.config.runner_ui.mappings.view_diff) do
+           open_viewer(map, "di")
+       end

        self.windows.tc:on(nui_event.CursorMoved, function()
            local tcindex = get_testcase_index_by_line()
@@ -264,6 +270,9 @@ function RunnerUI:show_viewer_popup(window_name)
            },
        }

+       if window_name == 'di' then
+           viewer_popup_settings.buf_options = {syntax = 'diff'}
+       end
        self.windows.vw = require("nui.popup")(viewer_popup_settings)
        self.windows.vw:mount()
        self.viewer_initialized = true
@@ -361,6 +370,7 @@ function RunnerUI:update_ui()
            set_buf_content(self.windows.eo.bufnr, data.expout)
            set_buf_content(self.windows.si.bufnr, data.stdin)
            set_buf_content(self.windows.se.bufnr, data.stderr)
+           set_buf_content(self.windows.di.bufnr, data.diff)
        end

        if self.make_viewer_visible then
diff --git a/lua/competitest/runner_ui/popup.lua b/lua/competitest/runner_ui/popup.lua
index 1e42629..db94492 100644
--- a/lua/competitest/runner_ui/popup.lua
+++ b/lua/competitest/runner_ui/popup.lua
@@ -112,6 +112,10 @@ function M.init_ui(windows, config)
    popup_settings.position = positions["se"]
    windows.se = nui_popup(popup_settings)

+   -- diff popup
+   popup_settings.border.text.top = " Diff "
+   windows.di = nui_popup(popup_settings)
+
    windows.so:mount()
    windows.eo:mount()
    windows.si:mount()
diff --git a/lua/competitest/runner_ui/split.lua b/lua/competitest/runner_ui/split.lua
index af71156..bb0a1da 100644
--- a/lua/competitest/runner_ui/split.lua
+++ b/lua/competitest/runner_ui/split.lua
@@ -32,6 +32,7 @@ function M.init_ui(windows, config, init_winid)
    settings.so = vim.deepcopy(split_settings)
    settings.se = vim.deepcopy(split_settings)
    settings.eo = vim.deepcopy(split_settings)
+   settings.di = vim.deepcopy(split_settings)

    ---Get first windows in the given layout
    ---@param layout table: layout description
@@ -111,6 +112,10 @@ function M.init_ui(windows, config, init_winid)
    vim.wo[windows[fw].winid]["winfixwidth"] = true
    vim.wo[windows[fw].winid]["winfixheight"] = true

+   -- create diff window. but don't mount it
+   settings.di.top = ' Diff '
+   windows.di = nui_split(settings.di)
+   
    local old_equalalways = vim.o.equalalways
    vim.o.equalalways = false
    create_layout(current_layout, windows[fw].winid, is_split_vertical)
MuhammadSawalhy commented 1 year ago

Awesome! But what about a diff view between Output and Expected output without extra windows like this?

image

shadmansaleh commented 1 year ago

But what about a diff view between Output and Expected output without extra windows like this?

Well we can just open the Output buffer and Expedcted Output buffer in two floats and run :diffthis on them.

Result: image

Patch:

diff --git a/lua/competitest/init.lua b/lua/competitest/init.lua
index a0eebdc..5a0eeaf 100644
--- a/lua/competitest/init.lua
+++ b/lua/competitest/init.lua
@@ -46,6 +46,7 @@ local default_config = {
            view_output = { "a", "A" },
            view_stdout = { "o", "O" },
            view_stderr = { "e", "E" },
+           view_diff = { "D", "<C-d>" },
            close = { "q", "Q" },
        },
        viewer = { -- viewer window, to view in detail a stream (input, expected output, stdout or stderr)
diff --git a/lua/competitest/runner_ui/init.lua b/lua/competitest/runner_ui/init.lua
index f4e1439..e25da2c 100644
--- a/lua/competitest/runner_ui/init.lua
+++ b/lua/competitest/runner_ui/init.lua
@@ -15,6 +15,8 @@ function RunnerUI:new(interface, restore_winid)
        ui_initialized = false,
        ui_visible = false,
        viewer_initialized = false,
+       diff_viewer_initialized = false,
+       diff_viewer_visible = false,
        viewer_visible = false,
        viewer_content = nil,
        restore_winid = restore_winid,
@@ -29,6 +31,8 @@ function RunnerUI:new(interface, restore_winid)
            eo = nil, -- expected output
            tc = nil, -- testcases selector
            vw = nil, -- viewer popup
+           dvwl = nil, -- diff view left
+           dvwr = nil, -- diff view right
        },
        tcdata = nil, -- table containing testcases data and results
    }
@@ -50,11 +54,13 @@ end
 function RunnerUI:resize_ui()
    local cursor_position = self.ui_visible and api.nvim_win_get_cursor(self.windows.tc.winid) -- restore cursor position later
    local was_viewer_visible = self.viewer_visible -- make viewer visible later
+   local was_diff_viewer_visible = self.diff_viewer_visible -- make viewer visible later
    self:delete()
    if cursor_position then -- if cursor_position isn't nil ui was visible
        self:show_ui()
        vim.schedule(function()
            self.make_viewer_visible = was_viewer_visible -- make update_ui() open viewer after updating details
+           self.make_diff_viewer_visible = was_diff_viewer_visible -- make update_ui() open diff viewer after updating details
            api.nvim_win_set_cursor(self.windows.tc.winid, cursor_position)
        end)
    end
@@ -75,7 +81,7 @@ function RunnerUI:show_ui()
            tc = "Testcases", -- testcases selector
        }
        for n, w in pairs(self.windows) do
-           if n ~= "vw" then
+           if n ~= "vw" and n ~= 'dvwr' and n ~= 'dvwl' then
                api.nvim_buf_set_var(w.bufnr, "competitest_title", windows_names[n])
                api.nvim_buf_set_name(w.bufnr, "CompetiTest" .. string.gsub(windows_names[n], " ", "") .. w.bufnr)
            end
@@ -93,6 +99,17 @@ function RunnerUI:show_ui()
                self.windows.vw:hide()
                api.nvim_set_current_win(self.windows.tc.winid)
                self.viewer_visible = false
+           elseif self.diff_viewer_visible then
+               vim.api.nvim_win_call(self.windows.dvwr.winid, function ()
+                   vim.cmd('diffoff')
+               end)
+               vim.api.nvim_win_call(self.windows.dvwl.winid, function ()
+                   vim.cmd('diffoff')
+               end)
+               self.windows.dvwl:hide()
+               self.windows.dvwr:hide()
+               api.nvim_set_current_win(self.windows.tc.winid)
+               self.diff_viewer_visible = false
            else
                self:hide_ui()
            end
@@ -101,7 +118,7 @@ function RunnerUI:show_ui()
        -- close windows keymaps
        for _, map in ipairs(self.runner.config.runner_ui.mappings.close) do
            for n, w in pairs(self.windows) do
-               if n ~= "vw" then
+               if n ~= "vw" and n ~= "dvwr" and n ~= "dvwl" then
                    w:map("n", map, hide_ui, { noremap = true })
                end
            end
@@ -170,6 +187,12 @@ function RunnerUI:show_ui()
        for _, map in ipairs(self.runner.config.runner_ui.mappings.view_stderr) do
            open_viewer(map, "se")
        end
+       -- view diff in a bigger window keymaps
+       for _, map in ipairs(self.runner.config.runner_ui.mappings.view_diff) do
+           self.windows.tc:map("n", map, function()
+               self:show_diff_viewer_popup()
+           end, { noremap = true })
+       end

        self.windows.tc:on(nui_event.CursorMoved, function()
            local tcindex = get_testcase_index_by_line()
@@ -201,6 +224,7 @@ function RunnerUI:hide_ui()
        end
        self.ui_visible = false
        self.viewer_visible = false
+       self.diff_viewer_visible = false
        api.nvim_set_current_win(self.restore_winid or 0)
    end
 end
@@ -216,7 +240,9 @@ function RunnerUI:delete()
    self.ui_initialized = false
    self.ui_visible = false
    self.viewer_initialized = false
+   self.diff_viewer_initialized = false
    self.viewer_visible = false
+   self.diff_viewer_visible = false
    api.nvim_set_current_win(self.restore_winid or 0)
 end

@@ -277,6 +303,63 @@ function RunnerUI:show_viewer_popup(window_name)
    api.nvim_set_current_win(self.windows.vw.winid)
 end

+---Open diff popup
+function RunnerUI:show_diff_viewer_popup()
+   if not self.diff_viewer_initialized then
+       local vim_width, vim_height = utils.get_ui_size()
+       local diff_viewer_popup_settings = {
+           bufnr = self.windows.so.bufnr,
+           zindex = 55, -- popup ui has zindex 50
+           border = {
+               style = self.runner.config.floating_border,
+               highlight = self.runner.config.floating_border_highlight,
+               text = {
+                   top = ' Output ',
+                   top_align = "center",
+               },
+           },
+           relative = "editor",
+           size = {
+               width = math.floor(vim_width * self.runner.config.runner_ui.viewer.width/2 + 0.5),
+               height = math.floor(vim_height * self.runner.config.runner_ui.viewer.height + 0.5),
+           },
+           position = {col = ((vim_width - math.floor(vim_width * self.runner.config.runner_ui.viewer.width)) / 2) - 2, row = '50%'},
+           win_options = {
+               number = self.runner.config.runner_ui.viewer.show_nu,
+               relativenumber = self.runner.config.runner_ui.viewer.show_rnu,
+           },
+       }
+
+       self.windows.dvwl = require("nui.popup")(diff_viewer_popup_settings)
+       self.windows.dvwl:mount()
+
+       diff_viewer_popup_settings.bufnr = self.windows.eo.bufnr
+       diff_viewer_popup_settings.border.text.top = ' Expected Output '
+       diff_viewer_popup_settings.position.col = diff_viewer_popup_settings.position.col + math.floor(vim_width * self.runner.config.runner_ui.viewer.width/2 ) + 2
+       self.windows.dvwr = require("nui.popup")(diff_viewer_popup_settings)
+       self.windows.dvwr:mount()
+       self.diff_viewer_initialized = true
+       self.diff_viewer_visible = true
+           vim.api.nvim_win_call(self.windows.dvwr.winid, function ()
+               vim.cmd('diffthis')
+           end)
+           vim.api.nvim_win_call(self.windows.dvwl.winid, function ()
+               vim.cmd('diffthis')
+           end)
+   elseif not self.diff_viewer_visible then
+       self.windows.dvwl:show()
+       self.windows.dvwr:show()
+       self.diff_viewer_visible = true
+       vim.api.nvim_win_call(self.windows.dvwr.winid, function ()
+           vim.cmd('diffthis')
+       end)
+       vim.api.nvim_win_call(self.windows.dvwl.winid, function ()
+           vim.cmd('diffthis')
+       end)
+   end
+   api.nvim_set_current_win(self.windows.dvwl.winid)
+end
+
 ---Return a string of length len, starting with str.
 ---If str's length is greater than len, str will be truncated
 ---Otherwise the remaining space will be filled with a fill char (fchar)
@@ -367,6 +450,10 @@ function RunnerUI:update_ui()
            self.make_viewer_visible = nil
            self:show_viewer_popup()
        end
+       if self.make_diff_viewer_visible then
+           self.make_diff_viewer_visible = nil
+           self:show_diff_viewer_popup()
+       end
    end)
 end

diff --git a/lua/competitest/runner_ui/popup.lua b/lua/competitest/runner_ui/popup.lua
index 1e42629..907b1a1 100644
--- a/lua/competitest/runner_ui/popup.lua
+++ b/lua/competitest/runner_ui/popup.lua
@@ -122,7 +122,7 @@ end
 -- Show popup UI
 function M.show_ui(windows)
    for n, w in pairs(windows) do
-       if n ~= "vw" then -- show ui but not viewer popup
+       if n ~= "vw" and n ~= "dvwl" and n ~= "dvwr" then -- show ui but not viewer popup
            w:show()
        end
    end

I'm not very familiar with the codebase nor am I familiar with the apis of nui. So this probably could have been done better. @xeluxee what do you think about adding this feature to competitest ?

xeluxee commented 1 year ago

@xeluxee what do you think about adding this feature to competitest ?

I'm definitely interested in adding this feature to CompetiTest, though at the moment I'm working on cloning contests (issue 5).

@shadmansaleh your patch looks interesting, but what do you think about a keybind to toggle diff highlights in Output buffer? So we don't need to open two floating windows to see diff

shadmansaleh commented 1 year ago

but what do you think about a keybind to toggle diff highlights in Output buffer? So we don't need to open two floating windows to see diff

We could do that. I opened separate floats because I often find output/expected output windows to be quite tiny. especially when nvim isn't on full screen. I thought it'd be easier to look at if it was to be shown in a larger window.

Though now looking at direct diff on output and expected output windows I don't think it's necessary.

xeluxee commented 1 year ago

I often find output/expected output windows to be quite tiny

You can enlarge them, give a look at layout customization

shadmansaleh commented 1 year ago

You can enlarge them, give a look at layout customization

Ooo didn't know that was possible. Thanks.

I've opened a pr implementing this feature. Do take a look at it as you get time.