mirror of
https://github.com/neovide/neovide.git
synced 2026-09-10 07:16:25 -04:00
First, we now set a sane default window title we already handle title updates from nvim, but the default startup behavior was still effectively "call everything Neovide" until otherwise. that's not very useful. now, if the user hasn't configured either 'title' or 'titlestring', we enable titles and use "%F" so the window follows the current path. *but we don't override user configuration. if either option was already set, we leave it alone. --- The second, "title-hidden" means "don't show the title", not "make the window stop having one" On macOS we got that wrong. when title hiding was enabled, we hid the title visually and then also **cleared** the underlying NSWindow title by setting it to an empty string. That broke the native bookkeeping for secondary windows. The window itself was created fine and the native tab showed up, but AppKit no longer had a real title to use for the Window menu entry. So the second window ended up effectively unnamed there. we fix it by leaving the NSWindow title alone. keep the title hidden from the titlebar, but preserve it for the platform APIs that actually need it.
415 lines
12 KiB
Lua
415 lines
12 KiB
Lua
---@class Args
|
|
---@field neovide_channel_id integer
|
|
---@field neovide_version string
|
|
---@field config_path string
|
|
---@field register_clipboard boolean
|
|
---@field register_right_click boolean
|
|
---@field remote boolean
|
|
---@field enable_focus_command boolean
|
|
---@field global_variable_settings string[]
|
|
---@field option_settings string[]
|
|
|
|
---@type Args
|
|
local args = ...
|
|
|
|
local M = {}
|
|
M.private = {}
|
|
|
|
vim.g.neovide_channel_id = args.neovide_channel_id
|
|
vim.g.neovide_version = args.neovide_version
|
|
|
|
-- Set some basic rendering options.
|
|
vim.o.lazyredraw = false
|
|
vim.o.termguicolors = true
|
|
|
|
local function set_default_window_title()
|
|
local title_info = vim.api.nvim_get_option_info2("title", {})
|
|
local titlestring_info = vim.api.nvim_get_option_info2("titlestring", {})
|
|
|
|
if title_info.was_set or titlestring_info.was_set then
|
|
return
|
|
end
|
|
|
|
vim.o.title = true
|
|
vim.o.titlestring = "%F"
|
|
end
|
|
|
|
set_default_window_title()
|
|
|
|
local function rpcnotify(method, ...)
|
|
vim.rpcnotify(vim.g.neovide_channel_id, method, ...)
|
|
end
|
|
|
|
local function rpcrequest(method, ...)
|
|
return vim.rpcrequest(vim.g.neovide_channel_id, method, ...)
|
|
end
|
|
|
|
local function set_clipboard(register)
|
|
return function(lines)
|
|
rpcrequest("neovide.set_clipboard", lines, register)
|
|
end
|
|
end
|
|
|
|
local function get_clipboard(register)
|
|
return function()
|
|
return rpcrequest("neovide.get_clipboard", register)
|
|
end
|
|
end
|
|
|
|
-- Quit when Command+Q is pressed on macOS
|
|
if vim.fn.has("macunix") then
|
|
vim.keymap.set({ "n", "i", "c", "v", "o", "t", "l" }, "<D-q>", function()
|
|
rpcnotify("neovide.exec_detach_handler")
|
|
end)
|
|
end
|
|
|
|
if args.register_clipboard and not vim.g.neovide_no_custom_clipboard then
|
|
vim.g.clipboard = {
|
|
name = "neovide",
|
|
copy = {
|
|
["+"] = set_clipboard("+"),
|
|
["*"] = set_clipboard("*"),
|
|
},
|
|
paste = {
|
|
["+"] = get_clipboard("+"),
|
|
["*"] = get_clipboard("*"),
|
|
},
|
|
cache_enabled = false,
|
|
}
|
|
vim.g.loaded_clipboard_provider = nil
|
|
vim.cmd.runtime("autoload/provider/clipboard.vim")
|
|
end
|
|
|
|
if args.register_right_click then
|
|
vim.api.nvim_create_user_command("NeovideRegisterRightClick", function()
|
|
rpcnotify("neovide.register_right_click")
|
|
end, {})
|
|
vim.api.nvim_create_user_command("NeovideUnregisterRightClick", function()
|
|
rpcnotify("neovide.unregister_right_click")
|
|
end, {})
|
|
end
|
|
|
|
vim.api.nvim_create_user_command("NeovideFocus", function()
|
|
rpcnotify("neovide.focus_window")
|
|
end, {})
|
|
|
|
|
|
if vim.fn.has("mac") == 1 then
|
|
local URL_PATTERN = "https?://[%w-_%.]+%.%w[%w-_%.%%%?%.:/+=&%%[%]#]*"
|
|
local compat_unpack = table.unpack or unpack
|
|
local function find_span(line, cursor_column, pattern, plain)
|
|
local start_pos = 1
|
|
local search_limit = #line + 1
|
|
for _ = 1, search_limit do
|
|
local match_start, match_end = line:find(pattern, start_pos, plain)
|
|
if not match_start then
|
|
break
|
|
end
|
|
if cursor_column >= match_start and cursor_column <= match_end then
|
|
return match_start, match_end
|
|
end
|
|
start_pos = match_end + 1
|
|
end
|
|
return nil, nil
|
|
end
|
|
|
|
local function detect_url(line, cursor_column)
|
|
local url_start, url_end = find_span(line, cursor_column, URL_PATTERN, false)
|
|
if not url_start then
|
|
return nil
|
|
end
|
|
return {
|
|
entity = line:sub(url_start, url_end),
|
|
col = url_start - 1,
|
|
kind = "url",
|
|
}
|
|
end
|
|
|
|
local function detect_file(line, cursor_column)
|
|
local cfile = vim.fn.expand("<cfile>")
|
|
if not cfile or cfile == "" then
|
|
return nil
|
|
end
|
|
local literal_start = find_span(line, cursor_column, cfile, true)
|
|
if not literal_start then
|
|
return nil
|
|
end
|
|
local absolute_path = vim.fn.fnamemodify(cfile, ":p")
|
|
if absolute_path == "" or not vim.loop.fs_stat(absolute_path) then
|
|
return nil
|
|
end
|
|
return {
|
|
entity = absolute_path,
|
|
col = literal_start - 1,
|
|
kind = "file",
|
|
}
|
|
end
|
|
|
|
local function detect_word(line, cursor_column)
|
|
local word, word_col =
|
|
compat_unpack(vim.fn.matchstrpos(line, [[\k*\%]] .. cursor_column .. [[c\k*]]))
|
|
if not word or word == "" then
|
|
return nil
|
|
end
|
|
return {
|
|
entity = word,
|
|
col = word_col,
|
|
kind = "text",
|
|
}
|
|
end
|
|
|
|
local ENTITY_DETECTORS = { detect_url, detect_file, detect_word }
|
|
|
|
local function detect_entity(line, cursor_col)
|
|
local cursor_column = cursor_col + 1
|
|
for _, detector in ipairs(ENTITY_DETECTORS) do
|
|
local match = detector(line, cursor_column)
|
|
if match then
|
|
return match
|
|
end
|
|
end
|
|
return {
|
|
entity = "",
|
|
col = cursor_col,
|
|
kind = "text",
|
|
}
|
|
end
|
|
|
|
local function take_entity_under_cursor()
|
|
local mouse_pos = vim.fn.getmousepos()
|
|
local guifont = vim.api.nvim_get_option("guifont")
|
|
local cursor = vim.api.nvim_win_get_cursor(0)
|
|
local line = vim.api.nvim_get_current_line()
|
|
|
|
local match = detect_entity(line, cursor[2])
|
|
local screenpos = vim.fn.screenpos(mouse_pos.winid, cursor[1], match.col + 1)
|
|
local screen_row = math.max(screenpos.row - 1, 0)
|
|
local screen_col = math.max(screenpos.col - 1, 0)
|
|
|
|
return screen_col, screen_row, match.entity, guifont, match.kind
|
|
end
|
|
|
|
vim.api.nvim_create_user_command("NeovideForceClick", function()
|
|
local col, row, entity, guifont, entity_kind = take_entity_under_cursor()
|
|
rpcnotify("neovide.force_click", col, row, entity, guifont, entity_kind)
|
|
end, {})
|
|
end
|
|
|
|
vim.api.nvim_exec2(
|
|
[[
|
|
function! WatchGlobal(variable, callback)
|
|
call dictwatcheradd(g:, a:variable, a:callback)
|
|
endfunction
|
|
]],
|
|
{ output = false }
|
|
)
|
|
|
|
for _, global_variable_setting in ipairs(args.global_variable_settings) do
|
|
local callback = function()
|
|
rpcnotify("setting_changed", global_variable_setting, vim.g["neovide_" .. global_variable_setting])
|
|
end
|
|
vim.fn.WatchGlobal("neovide_" .. global_variable_setting, callback)
|
|
end
|
|
|
|
for _, option_setting in ipairs(args.option_settings) do
|
|
vim.api.nvim_create_autocmd({ "OptionSet" }, {
|
|
pattern = option_setting,
|
|
once = false,
|
|
nested = true,
|
|
callback = function()
|
|
rpcnotify("option_changed", option_setting, vim.o[option_setting])
|
|
end,
|
|
})
|
|
end
|
|
|
|
-- Ignore initial values of lines and columns because they are set by neovim directly.
|
|
-- See https://github.com/neovide/neovide/issues/2300
|
|
vim.api.nvim_create_autocmd({ "VimEnter" }, {
|
|
once = true,
|
|
nested = true,
|
|
callback = function()
|
|
for _, option_setting in ipairs(args.option_settings) do
|
|
if option_setting ~= "lines" and option_setting ~= "columns" then
|
|
rpcnotify("option_changed", option_setting, vim.o[option_setting])
|
|
end
|
|
end
|
|
end,
|
|
})
|
|
|
|
-- Create auto command for retrieving exit code from neovim on quit.
|
|
vim.api.nvim_create_autocmd({ "VimLeavePre" }, {
|
|
pattern = "*",
|
|
once = true,
|
|
nested = true,
|
|
callback = function()
|
|
rpcrequest("neovide.quit", vim.v.exiting)
|
|
end,
|
|
})
|
|
|
|
M.private.dropfile = function(filename, tabs)
|
|
vim.api.nvim_cmd({
|
|
cmd = "drop",
|
|
args = { vim.fn.fnameescape(filename) },
|
|
-- Always open as the last tabpage
|
|
mods = tabs and { tab = #vim.api.nvim_list_tabpages() } or {},
|
|
}, {})
|
|
end
|
|
|
|
M.private.can_set_background = function()
|
|
local info = vim.api.nvim_get_option_info2("background", {})
|
|
-- Don't change the background if someone else has set it
|
|
if info.was_set and info.last_set_chan ~= args.neovide_channel_id then
|
|
return false
|
|
else
|
|
return true
|
|
end
|
|
end
|
|
|
|
M.disable_redraw = function()
|
|
-- Wrap inside pcall to avoid errors if Neovide disconnects
|
|
pcall(rpcnotify, "neovide.set_redraw", false)
|
|
end
|
|
|
|
M.enable_redraw = function()
|
|
-- Wrap inside pcall to avoid errors if Neovide disconnects
|
|
pcall(rpcnotify, "neovide.set_redraw", true)
|
|
end
|
|
|
|
vim.api.nvim_create_user_command("NeovideConfig", function()
|
|
if args.remote then
|
|
if vim.fn.filereadable(args.config_path) ~= 0 then
|
|
vim.notify(
|
|
"Neovide is running as a remote server. So the config file may not be on this machine.\n"
|
|
.. "Open it manually if you intend to edit the file anyway.\n"
|
|
.. "Config file location: "
|
|
.. args.config_path,
|
|
vim.log.levels.WARN,
|
|
{ title = "Neovide" }
|
|
)
|
|
end
|
|
else
|
|
vim.cmd('edit ' .. vim.fn.fnameescape(args.config_path))
|
|
end
|
|
end, {})
|
|
|
|
local function progress_bar(data)
|
|
-- Wrap inside pcall to avoid errors if Neovide disconnects.
|
|
pcall(rpcnotify, "neovide.progress_bar", data)
|
|
end
|
|
|
|
local function buffer_is_empty(bufnr)
|
|
if vim.api.nvim_buf_line_count(bufnr) ~= 1 then
|
|
return false
|
|
end
|
|
local first_line = vim.api.nvim_buf_get_lines(bufnr, 0, 1, true)[1]
|
|
return first_line == nil or first_line == ""
|
|
end
|
|
|
|
local function current_window_is_only_normal()
|
|
local current = vim.api.nvim_get_current_win()
|
|
local found
|
|
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
|
|
local config = vim.api.nvim_win_get_config(win)
|
|
if config.relative == "" then
|
|
if found ~= nil then
|
|
return false
|
|
end
|
|
found = win
|
|
end
|
|
end
|
|
return found ~= nil and found == current
|
|
end
|
|
|
|
local function shortmess_allows_intro()
|
|
return not string.find(vim.o.shortmess or "", "I", 1, true)
|
|
end
|
|
|
|
local function should_show_intro_banner()
|
|
local current_buffer = vim.api.nvim_get_current_buf()
|
|
local buffer_name = vim.api.nvim_buf_get_name(current_buffer)
|
|
local current_window = vim.api.nvim_get_current_win()
|
|
|
|
return buffer_is_empty(current_buffer)
|
|
and buffer_name == ""
|
|
and current_buffer == 1
|
|
and current_window == 1000
|
|
and current_window_is_only_normal()
|
|
and shortmess_allows_intro()
|
|
end
|
|
|
|
local intro_banner_state
|
|
|
|
local function notify_intro_state()
|
|
local allowed = false
|
|
local ok, result = pcall(should_show_intro_banner)
|
|
if ok then
|
|
allowed = result
|
|
end
|
|
|
|
if intro_banner_state ~= allowed then
|
|
intro_banner_state = allowed
|
|
pcall(rpcnotify, "neovide.intro_banner_allowed", allowed)
|
|
end
|
|
end
|
|
|
|
local function schedule_intro_state_check()
|
|
vim.schedule(notify_intro_state)
|
|
end
|
|
|
|
local intro_group = vim.api.nvim_create_augroup("NeovideIntroBanner", { clear = true })
|
|
|
|
vim.api.nvim_create_autocmd({
|
|
"VimEnter",
|
|
"BufEnter",
|
|
"BufFilePost",
|
|
"BufNewFile",
|
|
"BufReadPost",
|
|
"TextChanged",
|
|
"TextChangedI",
|
|
"WinEnter",
|
|
"WinNew",
|
|
"WinClosed",
|
|
"TabEnter",
|
|
"TabClosed",
|
|
}, {
|
|
group = intro_group,
|
|
callback = schedule_intro_state_check,
|
|
})
|
|
|
|
vim.api.nvim_create_autocmd("OptionSet", {
|
|
group = intro_group,
|
|
pattern = "shortmess",
|
|
callback = schedule_intro_state_check,
|
|
})
|
|
|
|
notify_intro_state()
|
|
|
|
pcall(vim.api.nvim_create_autocmd, 'Progress', {
|
|
group = vim.api.nvim_create_augroup('NeovideProgressBar', { clear = true }),
|
|
desc = 'Forward progress events to Neovide',
|
|
callback = function(ev)
|
|
if ev.data and ev.data.status == 'running' then
|
|
progress_bar({
|
|
percent = ev.data.percent or 0,
|
|
title = ev.data.title or "",
|
|
message = ev.data.message or "",
|
|
})
|
|
else
|
|
progress_bar({ percent = 100 })
|
|
end
|
|
end,
|
|
})
|
|
|
|
---@param preedit_raw_text string
|
|
---@param cursor_offset_start integer This values show the cursor begin position. The position is byte-wise indexed.
|
|
---@param cursor_offset_end integer This values show the cursor end position. The position is byte-wise indexed.
|
|
M.preedit_handler = function(preedit_raw_text, cursor_offset_start, cursor_offset_end) end
|
|
|
|
---@param commit_raw_text string
|
|
---@param commit_formatted_text string It's escaped.
|
|
M.commit_handler = function(commit_raw_text, commit_formatted_text)
|
|
vim.api.nvim_input(commit_formatted_text)
|
|
end
|
|
|
|
_G["neovide"] = M
|