Repository: neovim/neovim
Pull request #32719: feat(runtime): Lua ftplugin 'includeexpr'
Author: login: brianhuster
Created: 2025-03-04T08:05:15Z | Merged: 2025-03-17T22:41:07Z

Problem: Current `'includeexpr'` in runtime/ftplugin/lua.vim doesn't work with Nvim Lua.

Solution: Better includeexpr for Lua ftplugin

Closes: https://github.com/neovim/neovim/issues/32490

TODO:
- [x] Use `vim.loader.find()`
- [x] Update news.txt
- [x] Add tests 
~- [ ] Refer to `vim.lua_includeexpr` in `:h ft-lua`~


Diff:
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
index e74344cece1bf2..8886dbed8247f7 100644
--- a/runtime/doc/news.txt
+++ b/runtime/doc/news.txt
@@ -326,6 +326,8 @@ LUA
   `vim.o`, `vim.wo`, `vim.bo`, `vim.opt`, `vim.opt_local`, `vim.opt_global`,
   and `vim.fn`.
 • Documentation for |lua-bit|.
+• |gf| in Lua buffers can go to module in same repo, |runtime-search-path| and
+  |package.path|.
 • |vim.fs.rm()| can delete files and directories.
 • |vim.validate()| now has a new signature which uses less tables,
   is more performant and easier to read.
diff --git a/runtime/ftplugin/lua.lua b/runtime/ftplugin/lua.lua
index e0f7e95cf6741f..ef8f126fab2b75 100644
--- a/runtime/ftplugin/lua.lua
+++ b/runtime/ftplugin/lua.lua
@@ -1,9 +1,10 @@
 -- use treesitter over syntax
 vim.treesitter.start()
 
+vim.bo.includeexpr = 'v:lua.require"vim._ftplugin.lua".includeexpr()'
 vim.bo.omnifunc = 'v:lua.vim.lua_omnifunc'
 vim.wo[0][0].foldexpr = 'v:lua.vim.treesitter.foldexpr()'
 
 vim.b.undo_ftplugin = (vim.b.undo_ftplugin or '')
   .. '\n call v:lua.vim.treesitter.stop()'
-  .. '\n setl omnifunc< foldexpr<'
+  .. '\n setl omnifunc< foldexpr< includeexpr<'
diff --git a/runtime/lua/vim/_ftplugin/lua.lua b/runtime/lua/vim/_ftplugin/lua.lua
new file mode 100644
index 00000000000000..588433409ba014
--- /dev/null
+++ b/runtime/lua/vim/_ftplugin/lua.lua
@@ -0,0 +1,26 @@
+local M = {}
+
+--- @param module string
+---@return string
+function M.includeexpr(module)
+  ---@param fname string
+  ---@return boolean
+  local function filereadable(fname)
+    return vim.fn.filereadable(fname) == 1
+  end
+
+  local fname = module:gsub('%.', '/')
+
+  local root = vim.fs.root(vim.api.nvim_buf_get_name(0), 'lua') or vim.fn.getcwd()
+  for _, suf in ipairs { '.lua', '/init.lua' } do
+    local path = vim.fs.joinpath(root, 'lua', fname .. suf)
+    if filereadable(path) then
+      return path
+    end
+  end
+
+  local modInfo = vim.loader.find(module)[1]
+  return modInfo and modInfo.modpath or module
+end
+
+return M
diff --git a/test/functional/editor/ftplugin_spec.lua b/test/functional/editor/ftplugin_spec.lua
new file mode 100644
index 00000000000000..5b57f98d92fa47
--- /dev/null
+++ b/test/functional/editor/ftplugin_spec.lua
@@ -0,0 +1,85 @@
+local t = require('test.testutil')
+local n = require('test.functional.testnvim')()
+
+local exec_lua = n.exec_lua
+local command = n.command
+local eq = t.eq
+
+---@param type string
+---@return string
+local function stdpath(type)
+  return exec_lua(([[return vim.fs.normalize(vim.fn.stdpath("%s"))]]):format(type))
+end
+
+---@return string
+local function vimruntime()
+  return exec_lua [[ return vim.fs.normalize(vim.env.VIMRUNTIME) ]]
+end
+
+---@param module string
+---@return string
+local function lua_includeexpr(module)
+  return exec_lua(([[return require('vim._ftplugin.lua').includeexpr("%s")]]):format(module))
+end
+
+local root = exec_lua [[ return vim.fs.normalize(vim.fn.getcwd()) ]]
+
+describe("Lua 'includeexpr'", function()
+  setup(n.clear)
+  local temp_dir = t.tmpname(false)
+  before_each(function()
+    command(([[
+      edit `=stdpath('config') .. '/lua/user-foo/init.lua'`
+      write ++p
+      edit `=stdpath('config') .. '/lua/user-foo/bar.lua'`
+      write ++p
+      edit `=stdpath('data') .. '/site/pack/packer/start/plugin-foo/lua/plugin-foo/init.lua'`
+      write ++p
+      edit `=stdpath('data') .. '/site/pack/packer/start/plugin-foo/lua/plugin-foo/bar.lua'`
+      write ++p
+
+      edit runtime/lua/foo/init.lua
+      write ++p
+      edit runtime/lua/foo/bar/init.lua
+      write ++p
+
+      edit %s/lua/runtime-foo/init.lua
+      write ++p
+      edit %s/lua/runtime-foo/bar.lua
+      write ++p
+    ]]):format(temp_dir, temp_dir))
+  end)
+
+  it('finds module in current repo', function()
+    command [[ edit runtime/lua/vim/_ftplugin/lua.lua ]]
+    eq(root .. '/runtime/lua/vim/_ftplugin/lua.lua', lua_includeexpr('vim._ftplugin.lua'))
+    eq(root .. '/runtime/lua/editorconfig.lua', lua_includeexpr('editorconfig'))
+    eq(root .. '/runtime/lua/foo/init.lua', lua_includeexpr('foo'))
+    eq(root .. '/runtime/lua/foo/bar/init.lua', lua_includeexpr('foo.bar'))
+  end)
+
+  it('find module in packpath/start', function()
+    eq(
+      stdpath('data') .. '/site/pack/packer/start/plugin-foo/lua/plugin-foo/init.lua',
+      lua_includeexpr('plugin-foo')
+    )
+    eq(
+      stdpath('data') .. '/site/pack/packer/start/plugin-foo/lua/plugin-foo/bar.lua',
+      lua_includeexpr('plugin-foo.bar')
+    )
+  end)
+
+  it('finds module in $VIMRUNTIME', function()
+    command('edit ' .. root)
+    eq(vimruntime() .. '/lua/vim/_ftplugin/lua.lua', lua_includeexpr('vim._ftplugin.lua'))
+    eq(vimruntime() .. '/lua/editorconfig.lua', lua_includeexpr('editorconfig'))
+  end)
+
+  it('find module in runtimepath', function()
+    eq(stdpath('config') .. '/lua/user-foo/init.lua', lua_includeexpr('user-foo'))
+    eq(stdpath('config') .. '/lua/user-foo/bar.lua', lua_includeexpr('user-foo.bar'))
+    command('set rtp+=' .. temp_dir)
+    eq(temp_dir .. '/lua/runtime-foo/init.lua', lua_includeexpr('runtime-foo'))
+    eq(temp_dir .. '/lua/runtime-foo/bar.lua', lua_includeexpr('runtime-foo.bar'))
+  end)
+end)


Reviews:
user: login: zeertzjq
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T00:01:50Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T13:06:38Z
user: login: clason
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T13:11:24Z
user: login: zeertzjq
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T01:47:44Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-15T16:50:04Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-15T16:51:40Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-15T16:52:41Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-15T16:54:48Z
user: login: justinmk
body: LGTM after comments.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-15T17:05:45Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-15T18:52:44Z
user: login: brianhuster
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-16T01:18:58Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-16T13:28:54Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-16T13:32:44Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-16T13:35:50Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-16T13:35:58Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-16T14:42:50Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-16T16:27:45Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-16T16:30:08Z
user: login: brianhuster
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-16T17:51:16Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-16T19:01:09Z
user: login: brianhuster
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T03:35:57Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T11:44:51Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T11:46:32Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T11:48:27Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T11:48:53Z
user: login: brianhuster
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T17:35:45Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T22:43:04Z

Review comments:
diff_hunk: @@ -1142,6 +1142,7 @@ do
   end
 end
 
+
path: runtime/lua/vim/_editor.lua
user: login: zeertzjq
body: ```suggestion
```
created_at: 2025-03-12T00:01:50Z
author_association: MEMBER
diff_hunk: @@ -1,4 +1,8 @@
 -- use treesitter over syntax
 vim.treesitter.start()
 
-vim.b.undo_ftplugin = (vim.b.undo_ftplugin or '') .. '\n call v:lua.vim.treesitter.stop()'
+vim.bo.includeexpr = "v:lua.require'_ftplugin.lua'.includeexpr(v:fname)"
path: runtime/ftplugin/lua.lua
user: login: justinmk
body: Is `_ftplugin.lua` needed because `runtime/ftplugin/` is not on 'runtimepath' ? 

Rather than introducing `_ftplugin.lua`, can we use `loadfile(vim.env.VIMRUNTIME..'/ftplugin/lua.lua')` ?  This should work:

    vim.bo.includeexpr = "v:lua.loadfile($VIMRUNTIME..'/ftplugin/lua.lua')"
created_at: 2025-03-12T13:06:38Z
author_association: MEMBER
diff_hunk: @@ -1,4 +1,8 @@
 -- use treesitter over syntax
 vim.treesitter.start()
 
-vim.b.undo_ftplugin = (vim.b.undo_ftplugin or '') .. '\n call v:lua.vim.treesitter.stop()'
+vim.bo.includeexpr = "v:lua.require'_ftplugin.lua'.includeexpr(v:fname)"
path: runtime/ftplugin/lua.lua
user: login: clason
body: No, it's just an arbitrary path to avoid polluting the global namespace (and the top-level `runtime/lua` directory). It's not about _this_ `ftplugin` but about the "autoload" file for the includeexpr (which needs to live in a different file for this to work without headaches).

You can replace `require` with `loadfile`, but this will give you no benefit (and cost you `require`'s caching).
created_at: 2025-03-12T13:11:20Z
author_association: MEMBER
diff_hunk: @@ -84,3 +84,5 @@ tags
 /contrib/result-*
 
 CMakeUserPresets.json
+
+Xconfirm_write_ro
path: .gitignore
user: login: zeertzjq
body: No
created_at: 2025-03-14T01:47:44Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,105 @@
+local t = require('test.testutil')
+local n = require('test.functional.testnvim')()
+
+local exec_lua = n.exec_lua
+local eq = t.eq
+local root = n.exec_lua('return vim.fs.normalize(vim.fn.getcwd())')
+
+before_each(function()
+  n.command [[e `=stdpath('config') .. '/lua/user-foo/init.lua'`]]
+  n.command('w ++p')
+  n.command [[e `=stdpath('config') .. '/lua/user-foo/module-bar.lua'`]]
+  n.command('w ++p')
+
+  n.command [[e `=stdpath('data') .. '/site/pack/plugin/start/test-plugin/lua/pack-foo/init.lua'`]]
+  n.command('w ++p')
+  n.command [[e `=stdpath('data') .. '/site/pack/plugin/start/test-plugin/lua/pack-foo/module-bar.lua'`]]
+  n.command('w ++p')
+  n.clear()
+end)
+
+local function stdpath(type)
+  return exec_lua(string.format([[ return vim.fs.normalize(vim.fn.stdpath('%s')) ]], type))
+end
+
+local function vimruntime()
+  return exec_lua [[return vim.fs.normalize(vim.env.VIMRUNTIME)]]
+end
+
+describe('lua.includeexpr', function()
+  it('must returns path to module in the project', function()
+    n.command('e ' .. '/runtime/lua/_ftplugin/lua.lua')
+    eq(
+      root .. '/runtime/lua/_ftplugin/lua.lua',
+      exec_lua [[return require("_ftplugin.lua").includeexpr("_ftplugin.lua")]]
+    )
+    eq(
+      root .. '/runtime/lua/editorconfig.lua',
+      exec_lua [[return require("_ftplugin.lua").includeexpr("editorconfig")]]
+    )
+
+    n.command('e ' .. root .. '/runtime/lua/foo/init.lua')
+    n.command('w ++p')
+    n.command('e ' .. root .. '/runtime/lua/foo/bar/init.lua')
+    n.command('w ++p')
+    eq(
+      root .. '/runtime/lua/foo/init.lua',
+      exec_lua [[return require("_ftplugin.lua").includeexpr("foo")]]
+    )
+    eq(
+      root .. '/runtime/lua/foo/bar/init.lua',
+      exec_lua [[return require("_ftplugin.lua").includeexpr("foo.bar")]]
+    )
+  end)
+
+  it('must returns path to module in VIMRUNTIME', function()
+    n.command('e .')
+    eq(
+      vimruntime() .. '/lua/_ftplugin/lua.lua',
+      exec_lua [[return require("_ftplugin.lua").includeexpr("_ftplugin.lua")]]
+    )
+    eq(
+      vimruntime() .. '/lua/editorconfig.lua',
+      exec_lua [[return require("_ftplugin.lua").includeexpr("editorconfig")]]
+    )
+  end)
+
+  it('must returns path to module in packpath/start', function()
path: test/functional/lua/ftplugin_spec.lua
user: login: justinmk
body: other cases too

```suggestion
  it('returns path to module in packpath/start', function()
```
created_at: 2025-03-15T16:50:03Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,105 @@
+local t = require('test.testutil')
+local n = require('test.functional.testnvim')()
+
+local exec_lua = n.exec_lua
+local eq = t.eq
+local root = n.exec_lua('return vim.fs.normalize(vim.fn.getcwd())')
+
+before_each(function()
+  n.command [[e `=stdpath('config') .. '/lua/user-foo/init.lua'`]]
+  n.command('w ++p')
+  n.command [[e `=stdpath('config') .. '/lua/user-foo/module-bar.lua'`]]
+  n.command('w ++p')
+
+  n.command [[e `=stdpath('data') .. '/site/pack/plugin/start/test-plugin/lua/pack-foo/init.lua'`]]
+  n.command('w ++p')
+  n.command [[e `=stdpath('data') .. '/site/pack/plugin/start/test-plugin/lua/pack-foo/module-bar.lua'`]]
+  n.command('w ++p')
+  n.clear()
+end)
+
+local function stdpath(type)
+  return exec_lua(string.format([[ return vim.fs.normalize(vim.fn.stdpath('%s')) ]], type))
+end
+
+local function vimruntime()
+  return exec_lua [[return vim.fs.normalize(vim.env.VIMRUNTIME)]]
+end
+
+describe('lua.includeexpr', function()
+  it('must returns path to module in the project', function()
+    n.command('e ' .. '/runtime/lua/_ftplugin/lua.lua')
+    eq(
+      root .. '/runtime/lua/_ftplugin/lua.lua',
+      exec_lua [[return require("_ftplugin.lua").includeexpr("_ftplugin.lua")]]
+    )
+    eq(
+      root .. '/runtime/lua/editorconfig.lua',
+      exec_lua [[return require("_ftplugin.lua").includeexpr("editorconfig")]]
+    )
+
+    n.command('e ' .. root .. '/runtime/lua/foo/init.lua')
+    n.command('w ++p')
+    n.command('e ' .. root .. '/runtime/lua/foo/bar/init.lua')
+    n.command('w ++p')
+    eq(
+      root .. '/runtime/lua/foo/init.lua',
+      exec_lua [[return require("_ftplugin.lua").includeexpr("foo")]]
+    )
+    eq(
+      root .. '/runtime/lua/foo/bar/init.lua',
+      exec_lua [[return require("_ftplugin.lua").includeexpr("foo.bar")]]
+    )
+  end)
+
+  it('must returns path to module in VIMRUNTIME', function()
+    n.command('e .')
+    eq(
+      vimruntime() .. '/lua/_ftplugin/lua.lua',
+      exec_lua [[return require("_ftplugin.lua").includeexpr("_ftplugin.lua")]]
+    )
+    eq(
+      vimruntime() .. '/lua/editorconfig.lua',
+      exec_lua [[return require("_ftplugin.lua").includeexpr("editorconfig")]]
+    )
+  end)
+
+  it('must returns path to module in packpath/start', function()
+    n.command('e $HOME')
+    eq(
+      stdpath('data') .. '/site/pack/plugin/start/test-plugin/lua/pack-foo/init.lua',
+      exec_lua [[return require("_ftplugin.lua").includeexpr("pack-foo")]]
+    )
+    eq(
+      stdpath('data') .. '/site/pack/plugin/start/test-plugin/lua/pack-foo/module-bar.lua',
+      exec_lua [[return require("_ftplugin.lua").includeexpr("pack-foo.module-bar")]]
+    )
+  end)
+
+  it('must returns path to module in runtimepath', function()
+    n.command('e $HOME')
+    eq(
+      stdpath('config') .. '/lua/user-foo/init.lua',
+      exec_lua [[return require("_ftplugin.lua").includeexpr("user-foo")]]
+    )
+    eq(
+      stdpath('config') .. '/lua/user-foo/module-bar.lua',
+      exec_lua [[return require("_ftplugin.lua").includeexpr("user-foo.module-bar")]]
+    )
+
+    n.command [[e `=$HOME . '/.nvim/lua/runtime-foo/init.lua'`]]
+    n.command('w ++p')
path: test/functional/lua/ftplugin_spec.lua
user: login: justinmk
body: usually helpful for tests to use non-abbreviated command/option names.

also, could use a single, multiline `n.command[[...]]` for readability, for many parts of these tests?

```suggestion
    n.command('write ++p')
```
created_at: 2025-03-15T16:51:40Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,105 @@
+local t = require('test.testutil')
+local n = require('test.functional.testnvim')()
+
+local exec_lua = n.exec_lua
+local eq = t.eq
+local root = n.exec_lua('return vim.fs.normalize(vim.fn.getcwd())')
+
+before_each(function()
+  n.command [[e `=stdpath('config') .. '/lua/user-foo/init.lua'`]]
+  n.command('w ++p')
+  n.command [[e `=stdpath('config') .. '/lua/user-foo/module-bar.lua'`]]
+  n.command('w ++p')
+
+  n.command [[e `=stdpath('data') .. '/site/pack/plugin/start/test-plugin/lua/pack-foo/init.lua'`]]
+  n.command('w ++p')
+  n.command [[e `=stdpath('data') .. '/site/pack/plugin/start/test-plugin/lua/pack-foo/module-bar.lua'`]]
+  n.command('w ++p')
+  n.clear()
+end)
+
+local function stdpath(type)
+  return exec_lua(string.format([[ return vim.fs.normalize(vim.fn.stdpath('%s')) ]], type))
+end
+
+local function vimruntime()
+  return exec_lua [[return vim.fs.normalize(vim.env.VIMRUNTIME)]]
+end
+
+describe('lua.includeexpr', function()
path: test/functional/lua/ftplugin_spec.lua
user: login: justinmk
body: - option names are usually single-quoted by convention (or `&foo`)
- titlecase Lua (not "lua") when referring to the language name (as opposed to a programmatic symbol/filename/etc)

```suggestion
describe("Lua 'includeexpr'", function()
```
created_at: 2025-03-15T16:52:41Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,105 @@
+local t = require('test.testutil')
path: test/functional/lua/ftplugin_spec.lua
user: login: justinmk
body: this test file should probably live in `test/functional/editor/`. 

`lua/` tests are for the Lua stdlib. Not general features. The fact that this feature is implemented via `_ftplugin.lua` is an implementation detail.

Fix for `runtime_spec.lua` : 

- https://github.com/neovim/neovim/pull/32919
created_at: 2025-03-15T16:54:47Z
author_association: MEMBER
path: runtime/lua/_ftplugin/lua.lua
user: login: justinmk
body: Just noticed something else: to avoid conflicts with e.g. a plugin (or user config) that happens to be named `_ftplugin`, we should namespace any builtin Lua modules under `vim`. Thus this file should live at `runtime/lua/vim/_ftplugin/lua.lua`.

(Sorry if this conflicts with a previous suggestion that I made, though that wasn't intended.)

(Note: the top-level `editorconfig.lua`, `man.lua`, `tohtml.lua` plugins are "true plugins", that's why they have claimed their respective "namespaces". However, I wonder if we should reconsider that, maybe they should have a `nvim` or `vim` prefix. There's no real need for us to claim more and more top-level namespaces...)
created_at: 2025-03-15T18:52:44Z
author_association: MEMBER
path: runtime/lua/_ftplugin/lua.lua
user: login: brianhuster
body: > However, I wonder if we should reconsider that, maybe they should have a nvim or vim prefix.

I always think so. Users may want to use another implementation of the plugin.

Vim has a few plugins that use top-level namespace like `comment`, `editorconfig`, `helptoc`, but they are all optional plugins that must be "installed" explicitly with `packadd`, so they less likely conflict.

Edit: I forgot that Vim also has a bunch of `<ft>_complete.vim` files in `runtime/autoload/`
created_at: 2025-03-16T01:18:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -251,6 +251,10 @@ DEFAULTS
   • Lua |ftplugin| sets 'omnifunc' to "v:lua.vim.lua_omnifunc".
   • Lua |ftplugin| sets 'foldexpr' to "v:lua.vim.treesitter.foldexpr()".
 
+• Options:
+  • Lua |ftplugin| sets 'omnifunc' to "v:lua.vim.lua_omnifunc".
+  • Lua |ftplugin| sets 'foldexpr' to "v:lua.vim.treesitter.foldexpr()".
path: runtime/doc/news.txt
user: login: justinmk
body: looks like a duplicate.

also, there are now 3 "Options" sections. (look just above)
created_at: 2025-03-16T13:28:53Z
author_association: MEMBER
path: test/functional/editor/include_spec.lua
user: login: justinmk
body: the old name `ftplugin_spec.lua` was more discoverable. `include_spec.lua` is over-granular, and unlikely to be recognized when trying to decide where a test should live.

if `fplugin_spec.lua` grows into a very big file that should later be split into more-granular concerns, that's a problem we will solve when we come to it (if ever). until then, hyper-categorizing is counterproductive.
created_at: 2025-03-16T13:32:44Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,109 @@
+local t = require('test.testutil')
+local n = require('test.functional.testnvim')()
+
+local exec_lua = n.exec_lua
+local eq = t.eq
+local root = n.exec_lua('return vim.fs.normalize(vim.fn.getcwd())')
+
+before_each(function()
+  n.command [[
+    edit `=stdpath('config') .. '/lua/user-foo/init.lua'`
+    write ++p
+    edit `=stdpath('config') .. '/lua/user-foo/module-bar.lua'`
+    write ++p
+
+    edit `=stdpath('data') .. '/site/pack/plugin/start/test-plugin/lua/pack-foo/init.lua'`
+    write ++p
+    edit `=stdpath('data') .. '/site/pack/plugin/start/test-plugin/lua/pack-foo/module-bar.lua'`
+    write ++p
+  ]]
+end)
+
+local function stdpath(type)
+  return exec_lua(string.format([[ return vim.fs.normalize(vim.fn.stdpath('%s')) ]], type))
+end
+
+local function vimruntime()
+  return exec_lua [[return vim.fs.normalize(vim.env.VIMRUNTIME)]]
+end
+
+describe("Lua 'includeexpr'", function()
+  it('returns path to module in the project', function()
path: test/functional/editor/include_spec.lua
user: login: justinmk
body: ```suggestion
  it('finds module in the project', function()
```
created_at: 2025-03-16T13:35:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,109 @@
+local t = require('test.testutil')
+local n = require('test.functional.testnvim')()
+
+local exec_lua = n.exec_lua
+local eq = t.eq
+local root = n.exec_lua('return vim.fs.normalize(vim.fn.getcwd())')
+
+before_each(function()
+  n.command [[
+    edit `=stdpath('config') .. '/lua/user-foo/init.lua'`
+    write ++p
+    edit `=stdpath('config') .. '/lua/user-foo/module-bar.lua'`
+    write ++p
+
+    edit `=stdpath('data') .. '/site/pack/plugin/start/test-plugin/lua/pack-foo/init.lua'`
+    write ++p
+    edit `=stdpath('data') .. '/site/pack/plugin/start/test-plugin/lua/pack-foo/module-bar.lua'`
+    write ++p
+  ]]
+end)
+
+local function stdpath(type)
+  return exec_lua(string.format([[ return vim.fs.normalize(vim.fn.stdpath('%s')) ]], type))
+end
+
+local function vimruntime()
+  return exec_lua [[return vim.fs.normalize(vim.env.VIMRUNTIME)]]
+end
+
+describe("Lua 'includeexpr'", function()
+  it('returns path to module in the project', function()
+    n.command('edit ' .. '/runtime/lua/vim/_ftplugin/lua.lua')
+    eq(
+      root .. '/runtime/lua/vim/_ftplugin/lua.lua',
+      exec_lua [[return require("vim._ftplugin.lua").includeexpr("vim._ftplugin.lua")]]
+    )
+    eq(
+      root .. '/runtime/lua/editorconfig.lua',
+      exec_lua [[return require("vim._ftplugin.lua").includeexpr("editorconfig")]]
+    )
+
+    n.command('edit ' .. root .. '/runtime/lua/foo/init.lua')
+    n.command('write ++p')
+    n.command('edit ' .. root .. '/runtime/lua/foo/bar/init.lua')
+    n.command('write ++p')
+    eq(
+      root .. '/runtime/lua/foo/init.lua',
+      exec_lua [[return require("vim._ftplugin.lua").includeexpr("foo")]]
+    )
+    eq(
+      root .. '/runtime/lua/foo/bar/init.lua',
+      exec_lua [[return require("vim._ftplugin.lua").includeexpr("foo.bar")]]
+    )
+  end)
+
+  it('returns path to module in VIMRUNTIME', function()
path: test/functional/editor/include_spec.lua
user: login: justinmk
body: etc.

```suggestion
  it('finds module in VIMRUNTIME', function()
```
created_at: 2025-03-16T13:35:58Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,109 @@
+local t = require('test.testutil')
+local n = require('test.functional.testnvim')()
+
+local exec_lua = n.exec_lua
+local eq = t.eq
+local root = n.exec_lua('return vim.fs.normalize(vim.fn.getcwd())')
+
+before_each(function()
+  n.command [[
+    edit `=stdpath('config') .. '/lua/user-foo/init.lua'`
path: test/functional/editor/ftplugin_spec.lua
user: login: justinmk
body: this will only work if `clear()` was previously called in some other test. 

while we're here, I suggest moving this into the describe() block. that will be needed in the future anyway, if/when other ftplugin-related tests are added to this file.

```suggestion
setup(n.clear)
before_each(function()
  n.command [[
    edit `=stdpath('config') .. '/lua/user-foo/init.lua'`
```
created_at: 2025-03-16T14:42:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,115 @@
+local t = require('test.testutil')
+local n = require('test.functional.testnvim')()
+
+local exec_lua = n.exec_lua
+local command = n.command
+local eq = t.eq
+
+---@param type string
+---@return string
+local function stdpath(type)
+  return exec_lua(([[return vim.fs.normalize(vim.fn.stdpath("%s"))]]):format(type))
+end
+
+---@return string
+local function vimruntime()
+  return exec_lua [[ return vim.fs.normalize(vim.env.VIMRUNTIME) ]]
+end
+
+---@param module string
+---@return string
+local function lua_includeexpr(module)
+  return exec_lua(([[return require('vim._ftplugin.lua').includeexpr("%s")]]):format(module))
+end
+
+local root = exec_lua [[ return vim.fs.normalize(vim.fn.getcwd()) ]]
+
+describe("Lua 'includeexpr'", function()
+  setup(n.clear)
+  before_each(function()
+    command [[
+      edit `=stdpath('config') .. '/lua/user-foo/init.lua'`
path: test/functional/editor/ftplugin_spec.lua
user: login: justinmk
body: nit: usually `exe 'edit' ..` is the preferred way to do this. `=` expressions are somewhat deprecated.

also typically `fnameescape()` is used to avoid path quirks.


created_at: 2025-03-16T16:27:45Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,115 @@
+local t = require('test.testutil')
+local n = require('test.functional.testnvim')()
+
+local exec_lua = n.exec_lua
+local command = n.command
+local eq = t.eq
+
+---@param type string
+---@return string
+local function stdpath(type)
+  return exec_lua(([[return vim.fs.normalize(vim.fn.stdpath("%s"))]]):format(type))
+end
+
+---@return string
+local function vimruntime()
+  return exec_lua [[ return vim.fs.normalize(vim.env.VIMRUNTIME) ]]
+end
+
+---@param module string
+---@return string
+local function lua_includeexpr(module)
+  return exec_lua(([[return require('vim._ftplugin.lua').includeexpr("%s")]]):format(module))
+end
+
+local root = exec_lua [[ return vim.fs.normalize(vim.fn.getcwd()) ]]
+
+describe("Lua 'includeexpr'", function()
+  setup(n.clear)
+  before_each(function()
+    command [[
+      edit `=stdpath('config') .. '/lua/user-foo/init.lua'`
+      write ++p
+      edit `=stdpath('config') .. '/lua/user-foo/bar.lua'`
+      write ++p
+      edit `=stdpath('data') .. '/site/pack/packer/start/plugin-foo/lua/plugin-foo/init.lua'`
+      write ++p
+      edit `=stdpath('data') .. '/site/pack/packer/start/plugin-foo/lua/plugin-foo/bar.lua'`
+      write ++p
+
+      edit runtime/lua/foo/init.lua
+      write ++p
+      edit runtime/lua/foo/bar/init.lua
+      write ++p
+
+      edit ~/.nvim/lua/runtime-foo/init.lua
+      write ++p
+      edit ~/.nvim/lua/runtime-foo/bar.lua
path: test/functional/editor/ftplugin_spec.lua
user: login: justinmk
body: any reason not to put this in `test/functional/fixtures/` ?

or use `t.tmpname(true)`. generally not a good idea to mess with `~/` in tests.
created_at: 2025-03-16T16:30:08Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,115 @@
+local t = require('test.testutil')
+local n = require('test.functional.testnvim')()
+
+local exec_lua = n.exec_lua
+local command = n.command
+local eq = t.eq
+
+---@param type string
+---@return string
+local function stdpath(type)
+  return exec_lua(([[return vim.fs.normalize(vim.fn.stdpath("%s"))]]):format(type))
+end
+
+---@return string
+local function vimruntime()
+  return exec_lua [[ return vim.fs.normalize(vim.env.VIMRUNTIME) ]]
+end
+
+---@param module string
+---@return string
+local function lua_includeexpr(module)
+  return exec_lua(([[return require('vim._ftplugin.lua').includeexpr("%s")]]):format(module))
+end
+
+local root = exec_lua [[ return vim.fs.normalize(vim.fn.getcwd()) ]]
+
+describe("Lua 'includeexpr'", function()
+  setup(n.clear)
+  before_each(function()
+    command [[
+      edit `=stdpath('config') .. '/lua/user-foo/init.lua'`
path: test/functional/editor/ftplugin_spec.lua
user: login: brianhuster
body: Hm, that's sad for me, I actually hope Nvim would support those backtick expansion more, because IMO they are shorter so better for using in cmdline.

Recently Helix also add support expansing cmdline https://github.com/helix-editor/helix/pull/12527. I notice `sh%{}`, which is already covered by Vim's backtick expansion
created_at: 2025-03-16T17:51:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,115 @@
+local t = require('test.testutil')
+local n = require('test.functional.testnvim')()
+
+local exec_lua = n.exec_lua
+local command = n.command
+local eq = t.eq
+
+---@param type string
+---@return string
+local function stdpath(type)
+  return exec_lua(([[return vim.fs.normalize(vim.fn.stdpath("%s"))]]):format(type))
+end
+
+---@return string
+local function vimruntime()
+  return exec_lua [[ return vim.fs.normalize(vim.env.VIMRUNTIME) ]]
+end
+
+---@param module string
+---@return string
+local function lua_includeexpr(module)
+  return exec_lua(([[return require('vim._ftplugin.lua').includeexpr("%s")]]):format(module))
+end
+
+local root = exec_lua [[ return vim.fs.normalize(vim.fn.getcwd()) ]]
+
+describe("Lua 'includeexpr'", function()
+  setup(n.clear)
+  before_each(function()
+    command [[
+      edit `=stdpath('config') .. '/lua/user-foo/init.lua'`
path: test/functional/editor/ftplugin_spec.lua
user: login: justinmk
body: backtick-expansion won't be removed, it's just not common (and there are some quirks but I don't remember the details).

Note that inline Lua expressions are planned: https://github.com/neovim/neovim/issues/11306
created_at: 2025-03-16T19:01:09Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,115 @@
+local t = require('test.testutil')
+local n = require('test.functional.testnvim')()
+
+local exec_lua = n.exec_lua
+local command = n.command
+local eq = t.eq
+
+---@param type string
+---@return string
+local function stdpath(type)
+  return exec_lua(([[return vim.fs.normalize(vim.fn.stdpath("%s"))]]):format(type))
+end
+
+---@return string
+local function vimruntime()
+  return exec_lua [[ return vim.fs.normalize(vim.env.VIMRUNTIME) ]]
+end
+
+---@param module string
+---@return string
+local function lua_includeexpr(module)
+  return exec_lua(([[return require('vim._ftplugin.lua').includeexpr("%s")]]):format(module))
+end
+
+local root = exec_lua [[ return vim.fs.normalize(vim.fn.getcwd()) ]]
+
+describe("Lua 'includeexpr'", function()
+  setup(n.clear)
+  before_each(function()
+    command [[
+      edit `=stdpath('config') .. '/lua/user-foo/init.lua'`
path: test/functional/editor/ftplugin_spec.lua
user: login: brianhuster
body: So only `backtick=` is deprecated?
created_at: 2025-03-17T03:35:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,115 @@
+local t = require('test.testutil')
+local n = require('test.functional.testnvim')()
+
+local exec_lua = n.exec_lua
+local command = n.command
+local eq = t.eq
+
+---@param type string
+---@return string
+local function stdpath(type)
+  return exec_lua(([[return vim.fs.normalize(vim.fn.stdpath("%s"))]]):format(type))
+end
+
+---@return string
+local function vimruntime()
+  return exec_lua [[ return vim.fs.normalize(vim.env.VIMRUNTIME) ]]
+end
+
+---@param module string
+---@return string
+local function lua_includeexpr(module)
+  return exec_lua(([[return require('vim._ftplugin.lua').includeexpr("%s")]]):format(module))
+end
+
+local root = exec_lua [[ return vim.fs.normalize(vim.fn.getcwd()) ]]
+
+describe("Lua 'includeexpr'", function()
+  setup(n.clear)
+  before_each(function()
+    command [[
+      edit `=stdpath('config') .. '/lua/user-foo/init.lua'`
path: test/functional/editor/ftplugin_spec.lua
user: login: justinmk
body: Even in vim they are rarely used. It's just guidance. They aren't going to be removed.
created_at: 2025-03-17T11:44:51Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,85 @@
+local t = require('test.testutil')
+local n = require('test.functional.testnvim')()
+
+local exec_lua = n.exec_lua
+local command = n.command
+local eq = t.eq
+
+---@param type string
+---@return string
+local function stdpath(type)
+  return exec_lua(([[return vim.fs.normalize(vim.fn.stdpath("%s"))]]):format(type))
+end
+
+---@return string
+local function vimruntime()
+  return exec_lua [[ return vim.fs.normalize(vim.env.VIMRUNTIME) ]]
+end
+
+---@param module string
+---@return string
+local function lua_includeexpr(module)
+  return exec_lua(([[return require('vim._ftplugin.lua').includeexpr("%s")]]):format(module))
+end
+
+local root = exec_lua [[ return vim.fs.normalize(vim.fn.getcwd()) ]]
+
+describe("Lua 'includeexpr'", function()
+  setup(n.clear)
+  local temp_dir = exec_lua [[ return vim.fs.normalize(vim.fn.tempname()) ]]
path: test/functional/editor/ftplugin_spec.lua
user: login: justinmk
body: I intentionally suggested `t.tmpname()` because it has extra handling by the test runner. For example, it creates the temp dir in `./build/` and cleans it up later. 
created_at: 2025-03-17T11:46:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,85 @@
+local t = require('test.testutil')
+local n = require('test.functional.testnvim')()
+
+local exec_lua = n.exec_lua
+local command = n.command
+local eq = t.eq
+
+---@param type string
+---@return string
+local function stdpath(type)
+  return exec_lua(([[return vim.fs.normalize(vim.fn.stdpath("%s"))]]):format(type))
+end
+
+---@return string
+local function vimruntime()
+  return exec_lua [[ return vim.fs.normalize(vim.env.VIMRUNTIME) ]]
+end
+
+---@param module string
+---@return string
+local function lua_includeexpr(module)
+  return exec_lua(([[return require('vim._ftplugin.lua').includeexpr("%s")]]):format(module))
+end
+
+local root = exec_lua [[ return vim.fs.normalize(vim.fn.getcwd()) ]]
+
+describe("Lua 'includeexpr'", function()
+  setup(n.clear)
+  local temp_dir = exec_lua [[ return vim.fs.normalize(vim.fn.tempname()) ]]
+  before_each(function()
+    command(([[
+      edit `=stdpath('config') .. '/lua/user-foo/init.lua'`
+      write ++p
+      edit `=stdpath('config') .. '/lua/user-foo/bar.lua'`
+      write ++p
+      edit `=stdpath('data') .. '/site/pack/packer/start/plugin-foo/lua/plugin-foo/init.lua'`
+      write ++p
+      edit `=stdpath('data') .. '/site/pack/packer/start/plugin-foo/lua/plugin-foo/bar.lua'`
+      write ++p
+
+      edit runtime/lua/foo/init.lua
+      write ++p
+      edit runtime/lua/foo/bar/init.lua
+      write ++p
+
+      edit %s/lua/runtime-foo/init.lua
+      write ++p
+      edit %s/lua/runtime-foo/bar.lua
+      write ++p
+    ]]):format(temp_dir, temp_dir))
+  end)
+
+  it('find module in current repo', function()
path: test/functional/editor/ftplugin_spec.lua
user: login: justinmk
body: ```suggestion
  it('finds module in current repo', function()
```
created_at: 2025-03-17T11:48:27Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,85 @@
+local t = require('test.testutil')
+local n = require('test.functional.testnvim')()
+
+local exec_lua = n.exec_lua
+local command = n.command
+local eq = t.eq
+
+---@param type string
+---@return string
+local function stdpath(type)
+  return exec_lua(([[return vim.fs.normalize(vim.fn.stdpath("%s"))]]):format(type))
+end
+
+---@return string
+local function vimruntime()
+  return exec_lua [[ return vim.fs.normalize(vim.env.VIMRUNTIME) ]]
+end
+
+---@param module string
+---@return string
+local function lua_includeexpr(module)
+  return exec_lua(([[return require('vim._ftplugin.lua').includeexpr("%s")]]):format(module))
+end
+
+local root = exec_lua [[ return vim.fs.normalize(vim.fn.getcwd()) ]]
+
+describe("Lua 'includeexpr'", function()
+  setup(n.clear)
+  local temp_dir = exec_lua [[ return vim.fs.normalize(vim.fn.tempname()) ]]
+  before_each(function()
+    command(([[
+      edit `=stdpath('config') .. '/lua/user-foo/init.lua'`
+      write ++p
+      edit `=stdpath('config') .. '/lua/user-foo/bar.lua'`
+      write ++p
+      edit `=stdpath('data') .. '/site/pack/packer/start/plugin-foo/lua/plugin-foo/init.lua'`
+      write ++p
+      edit `=stdpath('data') .. '/site/pack/packer/start/plugin-foo/lua/plugin-foo/bar.lua'`
+      write ++p
+
+      edit runtime/lua/foo/init.lua
+      write ++p
+      edit runtime/lua/foo/bar/init.lua
+      write ++p
+
+      edit %s/lua/runtime-foo/init.lua
+      write ++p
+      edit %s/lua/runtime-foo/bar.lua
+      write ++p
+    ]]):format(temp_dir, temp_dir))
+  end)
+
+  it('find module in current repo', function()
+    command [[ edit runtime/lua/vim/_ftplugin/lua.lua ]]
+    eq(root .. '/runtime/lua/vim/_ftplugin/lua.lua', lua_includeexpr('vim._ftplugin.lua'))
+    eq(root .. '/runtime/lua/editorconfig.lua', lua_includeexpr('editorconfig'))
+    eq(root .. '/runtime/lua/foo/init.lua', lua_includeexpr('foo'))
+    eq(root .. '/runtime/lua/foo/bar/init.lua', lua_includeexpr('foo.bar'))
+  end)
+
+  it('find module in packpath/start', function()
+    eq(
+      stdpath('data') .. '/site/pack/packer/start/plugin-foo/lua/plugin-foo/init.lua',
+      lua_includeexpr('plugin-foo')
+    )
+    eq(
+      stdpath('data') .. '/site/pack/packer/start/plugin-foo/lua/plugin-foo/bar.lua',
+      lua_includeexpr('plugin-foo.bar')
+    )
+  end)
+
+  it('find module in Vimruntime', function()
path: test/functional/editor/ftplugin_spec.lua
user: login: justinmk
body: is this about $VIMRUNTIME specifically?
```suggestion
  it('finds module in $VIMRUNTIME', function()
```
created_at: 2025-03-17T11:48:53Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,115 @@
+local t = require('test.testutil')
+local n = require('test.functional.testnvim')()
+
+local exec_lua = n.exec_lua
+local command = n.command
+local eq = t.eq
+
+---@param type string
+---@return string
+local function stdpath(type)
+  return exec_lua(([[return vim.fs.normalize(vim.fn.stdpath("%s"))]]):format(type))
+end
+
+---@return string
+local function vimruntime()
+  return exec_lua [[ return vim.fs.normalize(vim.env.VIMRUNTIME) ]]
+end
+
+---@param module string
+---@return string
+local function lua_includeexpr(module)
+  return exec_lua(([[return require('vim._ftplugin.lua').includeexpr("%s")]]):format(module))
+end
+
+local root = exec_lua [[ return vim.fs.normalize(vim.fn.getcwd()) ]]
+
+describe("Lua 'includeexpr'", function()
+  setup(n.clear)
+  before_each(function()
+    command [[
+      edit `=stdpath('config') .. '/lua/user-foo/init.lua'`
path: test/functional/editor/ftplugin_spec.lua
user: login: brianhuster
body: I rewrite those lines like this
```vim
      exe "edit" stdpath('config') .. '/lua/user-foo/init.lua'
      write ++p
      exe "edit" stdpath('config') .. '/lua/user-foo/bar.lua'
      write ++p
      exe "edit" stdpath('data') .. '/site/pack/packer/start/plugin-foo/lua/plugin-foo/init.lua'
      write ++p
      exe "edit" stdpath('data') .. '/site/pack/packer/start/plugin-foo/lua/plugin-foo/bar.lua'
      write ++p
```
But the [test](https://github.com/neovim/neovim/actions/runs/13902956472/job/38899033330) gives error `Undefined variable config`. so I revert to using `backtick=`
created_at: 2025-03-17T17:35:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,9 +1,10 @@
 -- use treesitter over syntax
 vim.treesitter.start()
 
+vim.bo.includeexpr = 'v:lua.require"vim._ftplugin.lua".includeexpr()'
path: runtime/ftplugin/lua.lua
user: login: justinmk
body: For future reference: if/when options support function refs, then we could avoid the need for `vim._ftplugin`, like this:

```lua
vim.bo.includeexpr = function()
  ...
end
```
created_at: 2025-03-17T22:43:04Z
author_association: MEMBER

PR comments:
user: login: justinmk
created_at: 2025-03-07T16:02:53Z
body: why can't this live in the lua ftplugin ? why is it needed at the top level of `vim.` ? https://github.com/neovim/neovim/issues/32490#issuecomment-2676981414 

The existing `vim.lua_omnifunc` is not a pattern that we need to perpetuate. Eventually we should move it, if we find a better pattern (e.g. somewhere in ftplugin). 
author_association: MEMBER
user: login: clason
created_at: 2025-03-07T16:06:40Z
body: If need be, move it to a `runtime/lua/_ftplugin.lua` (exact name is placeholder, except for the mandatory underscore; if we expect more "autoload at home", maybe `/_ftplugin/lua.lua`).

There may be justification for the "autoload" pattern ("only require what you use, when you use it"), but indeed there is zero reason for polluting the `vim` namespace with these functions.

After 0.11, we could take a beat and clean up `vim.*` from all of the private helper functions we don't already plan on graduating eventually and move them out in a similar way. (We just have to be careful about polluting the global namespace for plugins and document this clearly.)
author_association: MEMBER
user: login: brianhuster
created_at: 2025-03-08T11:00:00Z
body: I want the function to be public, so that users can set `'includeexpr'` to it.

So maybe I should move it to `vim/ftplugin.lua` or `vim/lua.lua` file?
author_association: CONTRIBUTOR
user: login: clason
created_at: 2025-03-08T11:02:21Z
body: No, you don't. You just want to change the default `includeexpr` to a better one, no need to make it public.
author_association: MEMBER
user: login: brianhuster
created_at: 2025-03-08T14:26:31Z
body: You mean I should set it in `runtime/ftplugin/lua.lua`? I didn't think of that, because I think users may use Lua for other purposes than Nvim config and plugins.

Or maybe I should add a global variable to allow users to switch between Nvim Lua and normal Lua?
author_association: CONTRIBUTOR
user: login: clason
created_at: 2025-03-08T14:30:51Z
body: Then the function should be made smarter to be a strict improvement on the current Vimscript `includexpr`. Having to switch would be a dealbreaker here.
author_association: MEMBER
user: login: brianhuster
created_at: 2025-03-08T15:59:35Z
body: Another question. Should I also add a function to find root or should I just assume that the current directory is the root of the Lua project?
author_association: CONTRIBUTOR
user: login: brianhuster
created_at: 2025-03-16T15:15:48Z
body: Can you please help me resolve the conflict? I think I don't know Git enough
author_association: CONTRIBUTOR
user: login: justinmk
created_at: 2025-03-16T16:25:06Z
body: resolved conflict, but lint might fail because `runtime/ftplugin/lua.lua` does not end with a newline 😭  (because of github's online text editor...)

can fix this by doing `:write` on that file in nvim, then commit and push.
author_association: MEMBER
user: login: brianhuster
created_at: 2025-03-17T04:59:24Z
body: Btw, should I squash the merge commit?
author_association: CONTRIBUTOR
user: login: justinmk
created_at: 2025-03-17T22:41:30Z
body: Thanks for driving this to a good conclusion. Will be useful :)
author_association: MEMBER
user: login: justinmk
created_at: 2025-03-17T23:31:46Z
body: followup https://github.com/neovim/neovim/pull/32948
author_association: MEMBER