Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add minimap #220

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
125 changes: 125 additions & 0 deletions runtime/lua/uivonim/minimap.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
local M = {}
local notify_uivonim = require'uivonim'.notify_uivonim

-- Derived from
-- https://github.com/nvim-telescope/telescope.nvim/blob/c061c216bfe082384d542a487ce02e9aed6177df/lua/telescope/builtin/files.lua#L378-L422
local function get_lines_and_highlights(bufnr)
local filetype = vim.api.nvim_buf_get_option(bufnr, 'filetype')
filetype = require'nvim-treesitter.parsers'.ft_to_lang(filetype)

local parser_ok, parser = pcall(vim.treesitter.get_parser, bufnr, filetype)
local query_ok, query = pcall(vim.treesitter.get_query, filetype, 'highlights')
if (not parser_ok) or (not query_ok) then return end

local line_highlights = setmetatable({}, {
__index = function(t, k)
local obj = {}
rawset(t, k, obj)
return obj
end,
})

local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
local root = parser:parse()[1]:root()

local highlighter = vim.treesitter.highlighter.new(parser)
local highlighter_query = highlighter:get_query(filetype)

for id, node in query:iter_captures(root, bufnr, 0, -1) do
local hl = highlighter_query.hl_cache[id]
if hl then
local row1, col1, row2, col2 = node:range()

if row1 == row2 then
local row = row1 + 1

for index = col1, col2 do
line_highlights[row][index] = hl
end
else
local row = row1 + 1
for index = col1, #lines[row] do
line_highlights[row][index] = hl
end

while row < row2 + 1 do
row = row + 1

for index = 0, #lines[row] do
line_highlights[row][index] = hl
end
end
end
end
end

local new_lines = {}
local api = vim.api

for i, line in ipairs(lines) do
local row = i
local n = 0
new_lines[i] = {}

-- https://stackoverflow.com/a/832414
for char in line:gmatch('.') do
table.insert(new_lines[i],
{
char = char,
hl = line_highlights[row][n] and api.nvim_get_hl_by_name(line_highlights[row][n], true) or nil,
})
n = n + 1
end
end

return new_lines
end

local enabled = false
local visible = false

function M.disable()
enabled = false
vim.cmd [[ autocmd! UivonimMinimap ]]
end

function M.is_visible()
return visible
end

function M.enable()
enabled = true
vim.cmd [[
augroup UivonimMinimap
autocmd WinScrolled * silent lua if require'uivonim/minimap'.is_visible() then require'uivonim/minimap'.update() else require'uivonim/minimap'.show() end
autocmd BufEnter * silent lua require'uivonim/minimap'.show()
augroup end
]]
end

function M.show()
if enabled then
local lines_and_hls = get_lines_and_highlights(0)
if lines_and_hls then
local info = vim.fn.getwininfo(vim.fn.win_getid())[1]
notify_uivonim('minimap', lines_and_hls, { topline = info.topline, botline = info.botline })
visible = true
end
end
end

function M.update()
if enabled then
local info = vim.fn.getwininfo(vim.fn.win_getid())[1]
notify_uivonim('minimap-update', { topline = info.topline, botline = info.botline })
end
end

function M.hide()
if enabled then
notify_uivonim('minimap-hide')
visible = false
end
end

return M
5 changes: 4 additions & 1 deletion src/common/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ export const Events = {
modifyColorschemeLive: 'modifyColorschemeLive',
explorer: 'explorer',
updateNameplates: 'window.refresh',
lspDiagnostics: 'lspDiagnostics'
lspDiagnostics: 'lspDiagnostics',
minimap: 'minimap',
minimapHide: 'minimapHide',
minimapUpdate: 'minimapUpdate',
} as const

export const RedrawEvents = {
Expand Down
6 changes: 6 additions & 0 deletions src/main/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
src: url('../assets/roboto-mono.ttf');
}

div#minimap > div > div > span {
white-space: pre;
font-size: 4px;
font-family: var(--font);
}

/* TODO(smolck): HACK that breaks stuff maybe potentially */
#plugins > div {
width: 100%;
Expand Down
3 changes: 3 additions & 0 deletions src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,9 @@ function setupActionHandlers(instanceApi: InstanceApi) {
sendOn('explorer', Events.explorer)
sendOn('update-nameplates', Events.updateNameplates)
sendOn('diagnostics', Events.lspDiagnostics)
sendOn('minimap', Events.minimap)
sendOn('minimap-hide', Events.minimapHide)
sendOn('minimap-update', Events.minimapUpdate)

nvim.instanceApi.onAction('version', () =>
nvim.instanceApi.nvimCommand(`echo 'Uivonim v${app.getVersion()}'`)
Expand Down
134 changes: 134 additions & 0 deletions src/renderer/components/extensions/minimap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { Events, Invokables } from '../../../common/ipc'
import { asColor } from '../../../common/utils'
import { render } from 'inferno'
import { PluginRight } from '../plugin-container'
import { colors } from '../../render/highlight-attributes'
import { font } from '../../workspace'

// Derived from https://stackoverflow.com/q/5560248
const lightenOrDarkenColor = (col: string, amt: number) => {
var usePound = false
if (col[0] == '#') {
col = col.slice(1)
usePound = true
}

var num = parseInt(col, 16)

var r = (num >> 16) + amt

if (r > 255) r = 255
else if (r < 0) r = 0

var b = ((num >> 8) & 0x00ff) + amt

if (b > 255) b = 255
else if (b < 0) b = 0

var g = (num & 0x0000ff) + amt

if (g > 255) g = 255
else if (g < 0) g = 0

return (usePound ? '#' : '') + (g | (b << 8) | (r << 16)).toString(16)
}

let isVisible = false
const Minimap = ({ visible }: { visible: boolean }) => {
return (
<PluginRight
visible={visible}
setBackground={false}
extraStyle={{ background: colors.background }}
width={'150px'}
>
<canvas id='minimap-canvas' style='height: 100%;' />
</PluginRight>
)
}

const container = document.createElement('div')
container.id = 'minimap'
document.getElementById('plugins')!.appendChild(container)

let ctx: CanvasRenderingContext2D
let canvas: HTMLCanvasElement
let linesAndHighlightsCache: any[]
let viewportCache: any

const maxLines = 200

// https://stackoverflow.com/a/18053642
const onClick = (event: MouseEvent) => {
const rect = canvas.getBoundingClientRect()
const y = event.clientY - rect.top

const height = canvas.height / maxLines
const row =
Math.floor(y / height) +
(viewportCache.botline > maxLines ? viewportCache.botline - maxLines : 0)
window.api.invoke(Invokables.nvimJumpTo, { line: row })
}

const update = (viewport: any, linesAndHighlights?: any[]) => {
if (linesAndHighlights) linesAndHighlightsCache = linesAndHighlights
viewportCache = viewport

if (!isVisible) render(<Minimap visible={true} />, container)
if (!ctx || !canvas) {
canvas = document.getElementById('minimap-canvas') as HTMLCanvasElement
ctx = canvas.getContext('2d')!!
ctx.scale(window.devicePixelRatio, window.devicePixelRatio)

// https://stackoverflow.com/a/48309022
canvas.width = canvas.getBoundingClientRect().width
canvas.height = canvas.getBoundingClientRect().height

canvas.addEventListener('click', onClick)
}

// Background
ctx.beginPath()
ctx.fillStyle = colors.background
ctx.fillRect(0, 0, canvas.width, canvas.height)

const width = canvas.width / 80
const height = canvas.height / maxLines

// Viewport
ctx.beginPath()
ctx.fillStyle = lightenOrDarkenColor(colors.background, 30)
const needsAName = viewport.botline - maxLines
const y =
viewport.botline > maxLines
? (viewport.topline - needsAName) * height
: viewport.topline * height
ctx.fillRect(
0,
y,
canvas.width,
viewport.botline * height - viewport.topline * height
)

ctx.beginPath()
const start = viewport.botline > maxLines ? viewport.botline - maxLines : 0
const end = viewport.botline > maxLines ? viewport.botline : maxLines
linesAndHighlightsCache.slice(start, end).forEach((line: any[], row) => {
line.forEach((char, col) => {
ctx.fillStyle = (
char.hl ? asColor(char.hl.foreground) : colors.background
)!!
ctx.font = `2px ${font.face}`
ctx.fillText(char, col * width, row * height, width)
})
})
}

window.api.on(Events.minimap, (linesAndHighlights: any[], viewport) =>
update(viewport, linesAndHighlights)
)
window.api.on(Events.minimapUpdate, (viewport) => update(viewport))

window.api.on(Events.minimapHide, () =>
render(<Minimap visible={false} />, container)
)
8 changes: 6 additions & 2 deletions src/renderer/components/plugin-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ type PluginProps = {
extraStyle?: any
children?: any
id?: string
setBackground?: boolean
width?: string
}

// TODO(smolck): Consolidate all of these.
Expand Down Expand Up @@ -100,15 +102,17 @@ export const PluginRight = ({
visible,
id,
extraStyle,
width = '500px',
setBackground = false,
children,
}: PluginProps) => (
<div id={id} style={right as CSSProperties}>
<div
style={{
...dialog,
width: '500px',
width,
height: '100%',
background: 'var(--background-40)',
background: setBackground ? 'var(--background-40)' : undefined,
display: visible ? 'flex' : 'none',
'flex-flow': 'column',
'margin-top': 0,
Expand Down
1 change: 1 addition & 0 deletions src/renderer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ requestAnimationFrame(() => {
})

setTimeout(() => {
require('./components/extensions/minimap')
require('./components/extensions/buffers')
require('./components/extensions/color-picker')
require('./components/extensions/explorer')
Expand Down