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

lsp: Support goToDefinition from a Go file to a templ file #387

Open
guettli opened this issue Jan 2, 2024 · 26 comments · May be fixed by #932
Open

lsp: Support goToDefinition from a Go file to a templ file #387

guettli opened this issue Jan 2, 2024 · 26 comments · May be fixed by #932

Comments

@guettli
Copy link
Contributor

guettli commented Jan 2, 2024

If I ctrl-click on page() then ...

image

then I get to the autogenerated Go code:

image

It would be great if I could get to the templ-file instead:

image

@a-h
Copy link
Owner

a-h commented Jan 2, 2024

Yes, that would be much more useful!

If you're in a *.templ file, then the templ LSP is operating and will carry out a remapping operation from templ position -> go file position -> gopls -> templ position, as per here:

for i := 0; i < len(result); i++ {
if isTemplGoFile, templURI := convertTemplGoToTemplURI(result[i].URI); isTemplGoFile {
result[i].URI = templURI
result[i].Range = p.convertGoRangeToTemplRange(templURI, result[i].Range)
}
}

But currently, if you start from a *.go file, then gopls is operating and there's no templ code running at all. To rewrite the responses from gopls, templ would have to proxy all Go LSP operations (i.e. for all *.go files) and work out that if a _templ.go file was being referenced, then, it should look up the various source code mapping positions and switch it to *.templ instead. I'm not sure if replacing (as a proxy) gopls for all projects is ideal (higher risk?), but it would work.

It may also possible for the templ LSP to capture *_templ.go file usage too, and be able to ctrl+click on the function name to get to the *.templ file. (Worse UX, but maybe a good first step)

@joerdav
Copy link
Collaborator

joerdav commented Jan 3, 2024

I know you can have multiple LSPs attached to a file, but I wonder what would happen if you had 2 trying to respond to a jump to definition request.

@guettli
Copy link
Contributor Author

guettli commented Jan 7, 2024

I asked the gopls maintainers. Maybe they know a hook which templ could implement: golang/go#65001

@joerdav joerdav changed the title Hyperlink from Go code to templ file. lsp: Support goToDefinition from a Go file to a templ file Jan 30, 2024
@joerdav joerdav added enhancement New feature or request lsp NeedsFix Needs implementing labels Jan 30, 2024
@joerdav
Copy link
Collaborator

joerdav commented Jan 30, 2024

We have the following issue to implement the line directive #415, and then I think it's a case of closing this ticket and helping the gopls team implement golang/go#65001

@guettli
Copy link
Contributor Author

guettli commented Jan 31, 2024

@joerdav thank you for your work! This is great!

@joerdav
Copy link
Collaborator

joerdav commented Jan 31, 2024

Thank @af-md !

@meistertigran
Copy link

Here's a quick and dirty fix for anyone using neovim. It doesn't go directly to the location of the definition in the file, but at least it opens the correct file.

local go_to_definition = function()
    if vim.bo.filetype == "go" then
        vim.lsp.buf.definition({
            on_list = function(options)
                if options == nil or options.items == nil or #options.items == 0 then
                    return
                end

                local targetFile = options.items[1].filename
                local prefix = string.match(targetFile, "(.-)_templ%.go$")

                if prefix then
                    options.items[1].filename = prefix .. ".templ"
                end

                vim.fn.setqflist({}, ' ', options)
                vim.api.nvim_command('cfirst')
            end
        })
    else
        vim.lsp.buf.definition()
    end
end

and then in your key map

vim.keymap.set("n", "gd", go_to_definition)

Note that the check for being in a go buffer will run every time you try to go to definition. You probably would want to add this modification only if you are using templ a lot and should remember to remove it once the actual fix is implemented.

@lobsterchan27
Copy link

does anyone have a workaround in vscode? its not a big deal but a little annoying.

@Kody-Quintana
Copy link

Kody-Quintana commented Aug 24, 2024

Quick and dirty addition to @meistertigran's quick and dirty fix, this'll attempt to search for the the line in the templ file

local go_to_definition = function()
  if vim.bo.filetype == "go" then

    vim.lsp.buf.definition({
      on_list = function(options)
        if options == nil or options.items == nil or #options.items == 0 then
          return
        end

        local targetFile = options.items[1].filename
        local prefix = string.match(targetFile, "(.-)_templ%.go$")

        if prefix then
          local function_name = vim.fn.expand('<cword>')
          options.items[1].filename = prefix .. ".templ"

          vim.fn.setqflist({}, ' ', options)
          vim.api.nvim_command('cfirst')

          vim.api.nvim_command('silent! /templ ' .. function_name)
        else
          vim.lsp.buf.definition()
        end

      end
    })
  else
    vim.lsp.buf.definition()
  end
end

@Smithx10
Copy link

In Neovim I use Telescope. This works decent enough for me... I might change it since I'm annoyed that the telescope window pops up for a few ms.

  vim.keymap.set('n', '<leader>gm', function()
    local ts_utils = require 'nvim-treesitter.ts_utils'
    local node = ts_utils.get_node_at_cursor()
    if node == nil then
      return
    end
    if vim.bo.filetype == "go" then
      if node:parent():type() == "call_expression" then
        local funcName = vim.fn.expand("<cword>")
        require("telescope.builtin").live_grep {
          default_text = 'templ ' .. funcName .. "\\(",
          glob_pattern = '*.templ',
          on_complete = {
            function(picker)
              if picker.manager.linked_states.size == 1 then
                require("telescope.actions").select_default(picker.prompt_bufnr)
              end
            end
          }
        }
      end
    end
  end),

@Razvan00Rusu
Copy link

Given that the line compiler directive MR has been closed, is there any work being done to address this / new ideas on how this could be tackled properly?

In the meantime for VSCode users, I have quickly created a very small extension (with the help of ChatGPT) to redirect definitions in _templ.go files to .templ in the same directory - which should cover the large majority of cases (mine at least). Sharing it here in case it is useful to others, the repo and VSIX file to install the extension is here

@linear linear bot added Migrated and removed NeedsFix Needs implementing enhancement New feature or request Migrated lsp labels Sep 4, 2024
@catgoose
Copy link

catgoose commented Sep 7, 2024

Quick and dirty addition to @meistertigran's quick and dirty fix, this'll attempt to search for the the line in the templ file

local go_to_definition = function()
  if vim.bo.filetype == "go" then

    vim.lsp.buf.definition({
      on_list = function(options)
        if options == nil or options.items == nil or #options.items == 0 then
          return
        end

        local targetFile = options.items[1].filename
        local prefix = string.match(targetFile, "(.-)_templ%.go$")

        if prefix then
          local function_name = vim.fn.expand('<cword>')
          options.items[1].filename = prefix .. ".templ"

          vim.fn.setqflist({}, ' ', options)
          vim.api.nvim_command('cfirst')

          vim.api.nvim_command('silent! /templ ' .. function_name)
        else
          vim.lsp.buf.definition()
        end

      end
    })
  else
    vim.lsp.buf.definition()
  end
end

My approach is to override vim.lsp.buf.definition for go filetype in LspAttach event:

  local function go_goto_def()
    local old = vim.lsp.buf.definition
    local opts = {
      on_list = function(options)
        if options == nil or options.items == nil or #options.items == 0 then
          return
        end
        local targetFile = options.items[1].filename
        local prefix = string.match(targetFile, "(.-)_templ%.go$")
        if prefix then
          local function_name = vim.fn.expand("<cword>")
          options.items[1].filename = prefix .. ".templ"
          vim.fn.setqflist({}, " ", options)
          vim.api.nvim_command("cfirst")
          vim.api.nvim_command("silent! /templ " .. function_name)
        else
          old()
        end
      end,
    }
    vim.lsp.buf.definition = function(o)
      o = o or {}
      o = vim.tbl_extend("keep", o, opts)
      old(o)
    end
  end

  vim.api.nvim_create_autocmd("LspAttach", {
    group = vim.api.nvim_create_augroup("UserLspConfig", {}),
    callback = function(event)
      local bufopts = { noremap = true, silent = true, buffer = event.buf }
      if vim.bo.filetype == "go" then
        go_goto_def()
      end
      vim.keymap.set, ("n", "gd", vim.lsp.buf.definition, bufopts)
    end
  }

@catgoose
Copy link

catgoose commented Sep 8, 2024

https://github.com/catgoose/templ-goto-definition

I made this plugin if it makes it easier for anyone.

@AlexanderHott
Copy link

I am looking into a different approach. I think it might be easier (and more useful for other tools) to extend your editor to be able to respond to multiple textDocument/definition providers. If a single LSP responds with multiple places (such as typescript showing both the react component and memoed component), it will put them in a list where you can select between them. Here is a very basic lua script for collecting all the responses from definition requests. you can run it with :luafile <file>.lua in neovim (same thing as pressing gd)

local pickers = require("telescope.pickers")
local finders = require("telescope.finders")
local conf = require("telescope.config").values
local actions = require("telescope.actions")
local action_state = require("telescope.actions.state")

local locations = {}

local params = vim.lsp.util.make_position_params()
local results_lsp = {}
local results = {}
local timeout = 1000 -- ms

for _, client in pairs(vim.lsp.get_active_clients()) do
	if client.server_capabilities.definitionProvider then
		local request_result =
			client.request_sync("textDocument/definition", params, timeout, vim.api.nvim_get_current_buf())
		if request_result and request_result.result then
			table.insert(results_lsp, request_result.result)
		end
	end
end

for _, lsp_result in ipairs(results_lsp) do
	if lsp_result then
		vim.list_extend(results, lsp_result)
	end
end

for _, result in ipairs(results) do
	if result.targetUri then
		table.insert(locations, {
			filename = vim.uri_to_fname(result.targetUri),
			lnum = result.targetRange.start.line + 1,
			col = result.targetRange.start.character + 1,
			text = "Definition",
		})
	elseif result.uri then
		table.insert(locations, {
			filename = vim.uri_to_fname(result.uri),
			lnum = result.range.start.line + 1,
			col = result.range.start.character + 1,
			text = "Definition",
		})
	end
end

pickers
	.new({}, {
		prompt_title = "LSP Definitions",
		finder = finders.new_table({
			results = locations,
			entry_maker = function(entry)
				return {
					value = entry,
					display = entry.filename .. ":" .. entry.lnum .. ":" .. entry.col .. " " .. entry.text,
					ordinal = entry.filename .. " " .. entry.lnum .. " " .. entry.col .. " " .. entry.text,
					filename = entry.filename,
					lnum = entry.lnum,
					col = entry.col,
				}
			end,
		}),
		sorter = conf.generic_sorter({}),
		attach_mappings = function(prompt_bufnr, map)
			actions.select_default:replace(function()
				actions.close(prompt_bufnr)
				local selection = action_state.get_selected_entry()
				vim.api.nvim_command("edit " .. selection.filename)
				vim.api.nvim_win_set_cursor(0, { selection.lnum, selection.col - 1 })
			end)
			return true
		end,
	})
	:find()

@catgoose
Copy link

catgoose commented Sep 14, 2024 via email

@AlexanderHott
Copy link

I'm making an LSP that can jump from generated typescript http functions to the respective api route in go (inspired by how in https://trpc.io/ you can see type errors quickly when you make changes) and thought it was a similar use case. I do agree that in this specific case, you probably wouldn't want to jump to the generated code, but it seemed like a similar problem.

@lsl
Copy link

lsl commented Sep 14, 2024

Another vscode extension, this one nests definition providers and jumps to the templ definition if it finds one.

https://marketplace.visualstudio.com/items?itemName=lsl.vscode-templ-go-to-definition

https://github.com/lsl/vscode-templ-go-to-definition

ext install lsl.vscode-templ-go-to-definition

@noor-tg
Copy link

noor-tg commented Sep 24, 2024

I use helix. And it only depends on lsp. So how to implement this line option with templ and go lsp ? @joerdav

@joerdav
Copy link
Collaborator

joerdav commented Sep 24, 2024

I saw your comment in #476

@noor-tg I think the answer will be to run templ lsp on go files. I've been playing around with it to see if that's viable. At the moment the behaviour is odd, a goToDefinition request is handled by both templ lsp and gopls, so you end up with 2 new files open! I imagine there would be similar cases for other LSP actions, but I think this will be the answer.

Does helix support assigning multiple lsps to a file?

@noor-tg
Copy link

noor-tg commented Sep 24, 2024

I saw your comment in #476

@noor-tg I think the answer will be to run templ lsp on go files. I've been playing around with it to see if that's viable. At the moment the behaviour is odd, a goToDefinition request is handled by both templ lsp and gopls, so you end up with 2 new files open! I imagine there would be similar cases for other LSP actions, but I think this will be the answer.

Does helix support assigning multiple lsps to a file?

Yes helix support multiple lsp to file type

@noor-tg
Copy link

noor-tg commented Sep 24, 2024

But when I add templ lsp last it use golsp and go to generated file. And if I set it first, it shows error def not found

@joerdav
Copy link
Collaborator

joerdav commented Sep 24, 2024

But when I add templ lsp last it use golsp and go to generated file. And if I set it first, it shows error def not found

Yes, currently templ lsp rejects gotodefenition requests from anything but templ files.

I had to remove this in my local version to get this working, but I think there is more testing to be done to ensure running on go files doesn't have other odd effects.

@noor-tg
Copy link

noor-tg commented Sep 24, 2024

But when I add templ lsp last it use golsp and go to generated file. And if I set it first, it shows error def not found

Yes, currently templ lsp rejects gotodefenition requests from anything but templ files.

I had to remove this in my local version to get this working, but I think there is more testing to be done to ensure running on go files doesn't have other odd effects.

I used the same . and it working correctly. thanks @joerdav

@noor-tg
Copy link

noor-tg commented Sep 24, 2024

I'm making an LSP that can jump from generated typescript http functions to the respective api route in go (inspired by how in https://trpc.io/ you can see type errors quickly when you make changes) and thought it was a similar use case. I do agree that in this specific case, you probably wouldn't want to jump to the generated code, but it seemed like a similar problem.

@AlexanderHott any link to the mentioned project ?

@tris203 tris203 linked a pull request Sep 24, 2024 that will close this issue
@AlexanderHott
Copy link

I'm making an LSP that can jump from generated typescript http functions to the respective api route in go (inspired by how in https://trpc.io/ you can see type errors quickly when you make changes) and thought it was a similar use case. I do agree that in this specific case, you probably wouldn't want to jump to the generated code, but it seemed like a similar problem.

@AlexanderHott any link to the mentioned project ?

It's on the back burner right now, and not very done. My team paused our migration to a go backend, but when that picks up again, I think I'll start working on it. https://github.com/AlexanderHOtt/longjump

@mkote
Copy link

mkote commented Nov 30, 2024

Quick and di

For any LazyVim users who could not get catgoose's plugin to work, here's a solution that works for me. Put the following in lua/plugins/lsp.lua:

return {
  "neovim/nvim-lspconfig",
  opts = function(args)
    local go_to_definition = function()
      if vim.bo.filetype == "go" then
        vim.lsp.buf.definition({
          on_list = function(options)
            if options == nil or options.items == nil or #options.items == 0 then
              return
            end

            local targetFile = options.items[1].filename
            local prefix = string.match(targetFile, "(.-)_templ%.go$")

            if prefix then
              local function_name = vim.fn.expand("<cword>")
              options.items[1].filename = prefix .. ".templ"

              vim.fn.setqflist({}, " ", options)
              vim.api.nvim_command("cfirst")

              vim.api.nvim_command("silent! /templ " .. function_name)
            else
              vim.lsp.buf.definition()
            end
          end,
        })
      else
        vim.lsp.buf.definition()
      end
    end
    local function go_goto_def()
      if vim.bo.filetype == "go" then
        return go_to_definition()
      else
        return vim.lsp.buf.definition()
      end
    end
    local keys = require("lazyvim.plugins.lsp.keymaps").get()
    keys[#keys + 1] = { "gd", go_goto_def }
  end,
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.