Repository: a-h/templ

Issue:
bug: go to definition causes error in Neovim v0.10.x
## Description

When using the "Go to Definition" feature in Neovim with templ components, an error occurs specifically for the `@atoms.Input` component. This error doesn't happen for other components or when the file containing the `Input` component is already open. Importantly, this issue does not occur in Visual Studio Code, suggesting it might be specific to the Neovim integration.

## Code Example

WaitlistForm component:
```go
package organisms
import (
	"github.com/axzilla/goilerplate/components/atoms"
	"github.com/axzilla/goilerplate/forms"
)
templ WaitlistForm(values forms.Waitlist, errors map[string]string) {
	<form hx-post="/waitlist" class="flex flex-col gap-2 w-96" hx-swap="outerHTML">
		if errors["success"] != "" {
			<div role="success" class="alert border border-primary">
				<div class="flex flex-col gap-2 justify-center items-center text-center font-bold">
					<p>🚀 You're in! 🚀</p>
					<p>Smart move, future SaaS superstar! We'll buzz your inbox when it's time to launch.</p>
				</div>
			</div>
		}
		@atoms.Input(templ.Attributes{
			"value":       values.Email,
			"name":        "email",
			"placeholder": "Email",
		}, "", errors["email"])
		<button data-loading-disable type="submit" class="btn btn-primary">Let's Go</button>
	</form>
}
```

Input component:
```go
package atoms
templ Input(attrs templ.Attributes, label, err string) {
	<div class="flex flex-col gap-2">
		if label != "" {
			<p class="">{ label }</p>
		}
		<input { attrs... } class="input input-bordered"/>
		if err != "" {
			<p class="text-red-500">{ err }</p>
		}
	</div>
}
```

## Error Message

When trying to use "Go to Definition" on `@atoms.Input`, the following error occurs:

```
Error executing vim.schedule lua callback: ...ar/neovim/0.10.1/share/nvim/runtime/lua/vim/lsp/util.lua:1914: index out of range
stack traceback:
	[C]: in function '_str_byteindex_enc'
	...ar/neovim/0.10.1/share/nvim/runtime/lua/vim/lsp/util.lua:1914: in function 'locations_to_items'
	...eovim/0.10.1/share/nvim/runtime/lua/vim/lsp/handlers.lua:435: in function 'handler'
	.../neovim/0.10.1/share/nvim/runtime/lua/vim/lsp/client.lua:687: in function ''
	vim/_editor.lua: in function <vim/_editor.lua:0>
```

## Environment

- Neovim version: 0.10.1
- templ version: v0.2.747
- Operating System: macOS

## Additional Information

- The error only occurs for the `@atoms.Input` component in Neovim.
- "Go to Definition" works for other components in Neovim.
- The error doesn't occur if the file containing the `Input` component is already open in Neovim.
- This issue does not occur in Visual Studio Code, where "Go to Definition" works as expected.
- A minimal Neovim configuration has been tested, and the issue persists.

## Expected Behavior

"Go to Definition" should successfully navigate to the definition of the `Input` component without throwing an error, as it does in Visual Studio Code.

## Questions

1. Is there an error in the provided code that could be causing this issue?
2. Are there any known incompatibilities between templ and Neovim 0.10.1?
3. Could this be related to how the templ language server is interacting with Neovim's LSP client?

Any insights or suggestions for troubleshooting would be greatly appreciated. Thank you for your time and effort in addressing this issue.

Hints from issue comments:
@a-h: I'm still using Neovim v0.9.5. I'll have to try out Neovim v0.10.1 and see if I get the same issue.

I spotted some emojis in the text. Does the problem go away if there are no emojis? Emojis are multi-byte characters, so they cause a lot of "fun".

The reason I ask is that LSP positions were recently updated in the LSP spec: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocuments to now support UTF-8 encoding positions.

I did a bit of work on positions in May (https://github.com/a-h/templ/pull/712) around multi-byte characters throwing off LSP positions, so there's some good tests in there around it.

It could be that the LSP code in Neovim is now trying to do some sort of negotiation as to what position to use, and is getting confused about text position, since it works fine in Neovim v0.9.5.

The fix might be to populate the correct parts of the LSP client / server neogitation to tell Neovim to use UTF-8 for positions (I think that's what VS Code does).

If you feel like having a crack at solving the issue...

I'd start by enabling logging in the editor, see https://templ.guide/commands-and-tools/ide-support#enable-lsp-logging - that way, you can see _exactly_ what is being sent to the LSP. There's even a web server which shows you the mapping between the input templ file and the generated code etc.

However, the issue might also be in Neovim's LSP code.@axadrn: > I'm still using Neovim v0.9.5. I'll have to try out Neovim v0.10.1 and see if I get the same issue.
> 
> I spotted some emojis in the text. Does the problem go away if there are no emojis? Emojis are multi-byte characters, so they cause a lot of "fun".
> 
> The reason I ask is that LSP positions were recently updated in the LSP spec: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocuments to now support UTF-8 encoding positions.
> 
> I did a bit of work on positions in May (#712) around multi-byte characters throwing off LSP positions, so there's some good tests in there around it.
> 
> It could be that the LSP code in Neovim is now trying to do some sort of negotiation as to what position to use, and is getting confused about text position, since it works fine in Neovim v0.9.5.
> 
> The fix might be to populate the correct parts of the LSP client / server neogitation to tell Neovim to use UTF-8 for positions (I think that's what VS Code does).
> 
> If you feel like having a crack at solving the issue...
> 
> I'd start by enabling logging in the editor, see https://templ.guide/commands-and-tools/ide-support#enable-lsp-logging - that way, you can see _exactly_ what is being sent to the LSP. There's even a web server which shows you the mapping between the input templ file and the generated code etc.
> 
> However, the issue might also be in Neovim's LSP code.

First of all, thank you for your feedback. Unfortunately, downgrading to 0.9.5 or deleting the emojis has not helped so far.@a-h: Well, it's good to rule things out. If you are able to create the smallest possible reproduction repo, that would be useful. Then I can try to load the repo on my setup, and see if I can reproduce it.

It could also be an issue in some other plugins that you're running. Here's my setup and scripts in case something jumps out at you:

https://github.com/a-h/dotfiles/blob/master/.config/nixpkgs/nvim.nix
https://github.com/a-h/dotfiles/tree/master/.config/nvim/lua

The issue with Neovim config is that people have loads of stuff running. So, starting from a minimal config and building up can also help with identifying where the issue is coming from.@axadrn: So, this is what I have right now.

- nvim 0.9.5 installed with Bob
- minimal init.lua => [https://gist.github.com/axzilla/e5c606fed106f87a4bfe039a262307d4](https://gist.github.com/axzilla/e5c606fed106f87a4bfe039a262307d4)
- minimal Go + Templ repo => [https://github.com/axzilla/templ-minimal](https://github.com/axzilla/templ-minimal)

Unfortunately, still the same problem. Honestly, I don't know what else to try!@tris203: I noticed this issue this morning, and have  been doing some digging, I think there is an off-by-2 due to:
web server reports the go file to start:
```go
// Code generated by templ - DO NOT EDIT.

package templ

//lint:file-ignore SA4006 This context is only used if a nested component is present.

import "github.com/a-h/templ"
```

in neovim i can see the go file starts
```go
// Code generated by templ - DO NOT EDIT.

// templ: version: v0.2.747
package templ

//lint:file-ignore SA4006 This context is only used if a nested component is present.

import "github.com/a-h/templ"
```

we seem to be missing the blank line and the version line (_lines 2 and 3_) in the lsps understanding 
Mapping through the web URI is perfect, but is missing those two lines

EDIT; that said, behaviour persists with ```include-version=false``` so, maybe I am wrong
@a-h: Thanks for investigating. If you enable the templ LSP web server, you can look at the mapping between templ files and the Go code that's being used by the LSP.

There's even a side-by-side view of templ code to Go code, with highlighting when you mouseover go code.@tris203: Thanks @a-h 

yes, the webview looks perfect, so either its a neovim bug, or the lsp is passing something that neovim isn't expecting
I am looking at the data passed to nvim, and the problem comes from the fact that the "line" that neovim is trying to pass is blank

This comes from this code here:
https://github.com/neovim/neovim/blob/3a61f05dd2d6cb2ac9bca4795467f459595e58dd/runtime/lua/vim/lsp/util.lua#L1797

here is the content of the ```rows``` table in the example i am testing with
```lua
{ {
    ["end"] = <1>{
      character = 10,
      line = 4
    },
    location = {
      range = {
        ["end"] = <table 1>,
        start = <2>{
          character = 6,
          line = 4
        }
      },
      uri = "file:///home/tris/code/fileclass/templ/Home.templ"
    },
    start = <table 2>
  }, {
    ["end"] = <3>{
      character = 40,
      line = 106
    },
    location = {
      range = {
        ["end"] = <table 3>,
        start = <4>{
          character = 36,
          line = 106
        }
      },
      uri = "file:///home/tris/code/fileclass/templ/Home.templ"
    },
    start = <table 4>
  } }
```

Line 106 is out of range in Home.templ, and isn't relevant in the generated go file either, so its something to do with the way that neovim is mapping the line/uri references I think. I will take another look after lunch

Additionally from the templ lsp log the mapping looks correct
```
{"level":"info","ts":"2024-08-28T10:32:38+01:00","caller":"proxy/server.go:71","msg":"updatePosition: found","uri":"file:///home/tris/code/fileclass/templ/Home.templ","fromTempl":"4:8","toGo":"11:7"}
```

so something is getting messed up in the translation into neovim@tris203: ~~I might be going crazy, but the issue seems to be when the component has the same name as the file, although its not immediately clear why that would matter~~

scratch that, the issue is if your templ definition is on line 5. which is common with a single import, 
if you have a single import in your .templ file in import "package" format, then that's what breaks it for me.

minimal reproduction:
```templ
package templ

import "fmt"

templ Hello(name string) {
{ fmt.Sprintf("Hello %s", name) }
}
```
even wierder, this works
```templ
package templ

import "fmt"

//this works
templ Hello(name string) {
{ fmt.Sprintf("Hello %s", name) }
}

```

even more minimal reproduction
```templ
package templ

//comment1
//comment2
templ Hello(name string) {
<h1>Hello, { name }!</h1>
}
```

why does templ lsp hate line 5?

@a-h: Interesting. Maybe it's a result of the recent change to auto-fixup imports.

Maybe the line is getting removed.@a-h: We have a good reproduction of a similar issue (maybe even the same?) in https://github.com/a-h/templ/issues/911 @axadrn: The problem disappeared with the nightly version!@joerdav: I didn't see that this one was actually already closed.

Solution patch:
diff --git a/cmd/templ/lspcmd/lspdiff/lspdiff.go b/cmd/templ/lspcmd/lspdiff/lspdiff.go
index af67f86..653ee96 100644
--- a/cmd/templ/lspcmd/lspdiff/lspdiff.go
+++ b/cmd/templ/lspcmd/lspdiff/lspdiff.go
@@ -25,6 +25,10 @@ func CompletionList(expected, actual *protocol.CompletionList) string {
 	)
 }
 
+func References(expected, actual []protocol.Location) string {
+	return cmp.Diff(expected, actual)
+}
+
 func CompletionListContainsText(cl *protocol.CompletionList, text string) bool {
 	if cl == nil {
 		return false
diff --git a/cmd/templ/lspcmd/proxy/server.go b/cmd/templ/lspcmd/proxy/server.go
index 8ac3597..fe66894 100644
--- a/cmd/templ/lspcmd/proxy/server.go
+++ b/cmd/templ/lspcmd/proxy/server.go
@@ -939,7 +939,6 @@ func (p *Server) RangeFormatting(ctx context.Context, params *lsp.DocumentRangeF
 func (p *Server) References(ctx context.Context, params *lsp.ReferenceParams) (result []lsp.Location, err error) {
 	p.Log.Info("client -> server: References")
 	defer p.Log.Info("client -> server: References end")
-	templURI := params.TextDocument.URI
 	// Rewrite the request.
 	var ok bool
 	ok, params.TextDocument.URI, params.Position = p.updatePosition(params.TextDocument.URI, params.Position)
@@ -954,8 +953,12 @@ func (p *Server) References(ctx context.Context, params *lsp.ReferenceParams) (r
 	// Rewrite the response.
 	for i := 0; i < len(result); i++ {
 		r := result[i]
-		r.URI = templURI
-		r.Range = p.convertGoRangeToTemplRange(templURI, r.Range)
+		isTemplURI, templURI := convertTemplGoToTemplURI(r.URI)
+		if isTemplURI {
+			p.Log.Info(fmt.Sprintf("references-%d - range conversion for %s", i, r.URI))
+			r.URI, r.Range = templURI, p.convertGoRangeToTemplRange(templURI, r.Range)
+		}
+		p.Log.Info(fmt.Sprintf("references-%d: %+v", i, r))
 		result[i] = r
 	}
 	return result, err



Test patch:
diff --git a/cmd/templ/lspcmd/lsp_test.go b/cmd/templ/lspcmd/lsp_test.go
index 6e9f914..2331e49 100644
--- a/cmd/templ/lspcmd/lsp_test.go
+++ b/cmd/templ/lspcmd/lsp_test.go
@@ -319,6 +319,142 @@ func TestHover(t *testing.T) {
 	}
 }
 
+func TestReferences(t *testing.T) {
+	if testing.Short() {
+		return
+	}
+
+	ctx, cancel := context.WithCancel(context.Background())
+	log, _ := zap.NewProduction()
+
+	ctx, appDir, _, server, teardown, err := Setup(ctx, log)
+	if err != nil {
+		t.Fatalf("failed to setup test: %v", err)
+		return
+	}
+	defer teardown(t)
+	defer cancel()
+
+	templFile, err := os.ReadFile(appDir + "/templates.templ")
+	if err != nil {
+		t.Fatalf("failed to read file %q: %v", appDir+"/templates.templ", err)
+		return
+
+	}
+	err = server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{
+		TextDocument: protocol.TextDocumentItem{
+			URI:        uri.URI("file://" + appDir + "/templates.templ"),
+			LanguageID: "templ",
+			Version:    1,
+			Text:       string(templFile),
+		},
+	})
+	if err != nil {
+		t.Errorf("failed to register open file: %v", err)
+		return
+	}
+	log.Info("Calling References")
+
+	tests := []struct {
+		line      int
+		character int
+		assert    func(t *testing.T, l []protocol.Location) (msg string, ok bool)
+	}{
+		{
+			// this is the definition of the templ function in the templates.templ file.
+			line:      5,
+			character: 9,
+			assert: func(t *testing.T, actual []protocol.Location) (msg string, ok bool) {
+				expectedReference := []protocol.Location{
+					{
+						// This is the useage of the templ function in the main.go file.
+						URI: uri.URI("file://" + appDir + "/main.go"),
+						Range: protocol.Range{
+							Start: protocol.Position{
+								Line:      uint32(24),
+								Character: uint32(7),
+							},
+							End: protocol.Position{
+								Line:      uint32(24),
+								Character: uint32(11),
+							},
+						},
+					},
+				}
+				if diff := lspdiff.References(expectedReference, actual); diff != "" {
+					return fmt.Sprintf("Expected: %+v\nActual: %+v", expectedReference, actual), false
+				}
+				return "", true
+			},
+		},
+		{
+			// this is the definition of the struct in the templates.templ file.
+			line:      21,
+			character: 9,
+			assert: func(t *testing.T, actual []protocol.Location) (msg string, ok bool) {
+				expectedReference := []protocol.Location{
+					{
+						// This is the useage of the struct in the templates.templ file.
+						URI: uri.URI("file://" + appDir + "/templates.templ"),
+						Range: protocol.Range{
+							Start: protocol.Position{
+								Line:      uint32(24),
+								Character: uint32(8),
+							},
+							End: protocol.Position{
+								Line:      uint32(24),
+								Character: uint32(14),
+							},
+						},
+					},
+				}
+				if diff := lspdiff.References(expectedReference, actual); diff != "" {
+					return fmt.Sprintf("Expected: %+v\nActual: %+v", expectedReference, actual), false
+				}
+				return "", true
+			},
+		},
+	}
+
+	for i, test := range tests {
+		t.Run(fmt.Sprintf("test-%d", i), func(t *testing.T) {
+			// Give CI/CD pipeline executors some time because they're often quite slow.
+			var ok bool
+			var msg string
+			for i := 0; i < 3; i++ {
+				if err != nil {
+					t.Error(err)
+					return
+				}
+				actual, err := server.References(ctx, &protocol.ReferenceParams{
+					TextDocumentPositionParams: protocol.TextDocumentPositionParams{
+						TextDocument: protocol.TextDocumentIdentifier{
+							URI: uri.URI("file://" + appDir + "/templates.templ"),
+						},
+						// Positions are zero indexed.
+						Position: protocol.Position{
+							Line:      uint32(test.line - 1),
+							Character: uint32(test.character - 1),
+						},
+					},
+				})
+				if err != nil {
+					t.Errorf("failed to get references: %v", err)
+					return
+				}
+				msg, ok = test.assert(t, actual)
+				if !ok {
+					break
+				}
+				time.Sleep(time.Millisecond * 500)
+			}
+			if !ok {
+				t.Error(msg)
+			}
+		})
+	}
+}
+
 func TestCodeAction(t *testing.T) {
 	if testing.Short() {
 		return