v0.50.0 c4ec31f

lsp_test.go RawHistoryFolder

1package lsp
2
3import (
4	"bufio"
5	"bytes"
6	"context"
7	"encoding/json"
8	"errors"
9	"io"
10	"os"
11	"path/filepath"
12	"runtime"
13	"strconv"
14	"strings"
15	"testing"
16	"time"
17)
18
19func TestFakeLSPProcess(t *testing.T) {
20	if os.Getenv("ZUT_FAKE_LSP") != "1" {
21		return
22	}
23	reader := bufio.NewReader(os.Stdin)
24	for {
25		payload, err := ReadMessage(reader)
26		if err != nil {
27			return
28		}
29		var message rpcEnvelope
30		if json.Unmarshal(payload, &message) != nil {
31			continue
32		}
33		switch message.Method {
34		case "initialize":
35			if os.Getenv("ZUT_FAKE_LSP_HANG_INITIALIZE") == "1" {
36				continue
37			}
38			response := []byte(`{"jsonrpc":"2.0","id":` + string(message.ID) + `,"result":{"capabilities":{"definitionProvider":true}}}`)
39			_ = WriteMessage(os.Stdout, response)
40		case "test/echo":
41			response := []byte(`{"jsonrpc":"2.0","id":` + string(message.ID) + `,"result":{"echo":true}}`)
42			_ = WriteMessage(os.Stdout, response)
43		case "shutdown":
44			response := []byte(`{"jsonrpc":"2.0","id":` + string(message.ID) + `,"result":null}`)
45			_ = WriteMessage(os.Stdout, response)
46		case "textDocument/didOpen":
47			var params struct {
48				TextDocument struct {
49					URI string `json:"uri"`
50				} `json:"textDocument"`
51			}
52			_ = json.Unmarshal(message.Params, &params)
53			response := map[string]any{"jsonrpc": "2.0", "method": "textDocument/publishDiagnostics", "params": map[string]any{"uri": params.TextDocument.URI, "diagnostics": []any{map[string]any{"range": map[string]any{"start": map[string]int{"line": 0, "character": 0}, "end": map[string]int{"line": 0, "character": 1}}, "severity": 1, "code": "FAKE", "message": "fake problem"}}}}
54			data, _ := json.Marshal(response)
55			_ = WriteMessage(os.Stdout, data)
56		}
57	}
58}
59
60func TestManagerCapabilitiesHonorsContextDuringInitialize(t *testing.T) {
61	root := t.TempDir()
62	config := `{"autoDetect":false,"servers":{"fake":{"kind":"lsp","command":` + strconv.Quote(os.Args[0]) + `,"args":["-test.run=TestFakeLSPProcess"],"env":{"ZUT_FAKE_LSP":"1","ZUT_FAKE_LSP_HANG_INITIALIZE":"1"}}}}`
63	if err := os.WriteFile(filepath.Join(root, "lsp.json"), []byte(config), 0o644); err != nil {
64		t.Fatal(err)
65	}
66	manager := NewManager()
67	t.Cleanup(func() { _ = manager.Close() })
68	ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
69	defer cancel()
70	started := time.Now()
71	_, err := manager.Capabilities(ctx, root, "", "fake")
72	if !errors.Is(err, context.DeadlineExceeded) {
73		t.Fatalf("Capabilities error = %v, want context deadline exceeded", err)
74	}
75	if elapsed := time.Since(started); elapsed > time.Second {
76		t.Fatalf("Capabilities returned after %s, want prompt context cancellation", elapsed)
77	}
78}
79
80func TestClientFakeStdioRPC(t *testing.T) {
81	root := t.TempDir()
82	script := os.Args[0]
83	spec := ServerConfig{ID: "fake", Kind: "lsp", Command: script, Args: []string{"-test.run=TestFakeLSPProcess"}, Env: map[string]string{"ZUT_FAKE_LSP": "1"}}
84	diagnostics := make(chan []Diagnostic, 1)
85	client, err := NewClientWithContext(context.Background(), spec, root, ClientOptions{OnDiagnostics: func(value []Diagnostic) { diagnostics <- value }})
86	if err != nil {
87		t.Fatal(err)
88	}
89	t.Cleanup(func() { _ = client.Close() })
90	result, err := client.Request(context.Background(), "test/echo", map[string]any{})
91	if err != nil || !bytes.Contains(result, []byte(`"echo":true`)) {
92		t.Fatalf("echo result = %s, err = %v", result, err)
93	}
94	path := filepath.Join(root, "main.go")
95	if err := os.WriteFile(path, []byte("package main\n"), 0o644); err != nil {
96		t.Fatal(err)
97	}
98	if err := client.DidOpen(path, "go", "package main\n"); err != nil {
99		t.Fatal(err)
100	}
101	select {
102	case values := <-diagnostics:
103		if len(values) != 1 || values[0].Code != "FAKE" || values[0].Path != path {
104			t.Fatalf("diagnostics = %#v", values)
105		}
106	case <-time.After(2 * time.Second):
107		t.Fatal("timed out waiting for fake diagnostics")
108	}
109}
110
111func TestFileURIPathRoundTrip(t *testing.T) {
112	path := filepath.Join(t.TempDir(), "space name", "main.go")
113	uri, err := pathToURI(path)
114	if err != nil {
115		t.Fatal(err)
116	}
117	if !strings.HasPrefix(uri, "file:///") {
118		t.Fatalf("URI = %q, want an absolute file URI", uri)
119	}
120	got, err := uriToPath(uri)
121	if err != nil {
122		t.Fatal(err)
123	}
124	abs, err := filepath.Abs(path)
125	if err != nil {
126		t.Fatal(err)
127	}
128	if filepath.Clean(got) != filepath.Clean(abs) {
129		t.Fatalf("round-trip path = %q, want %q", got, abs)
130	}
131	if runtime.GOOS != "windows" {
132		posixPath, err := uriToPath("file:///x:tmp/main.go")
133		if err != nil || posixPath != "/x:tmp/main.go" {
134			t.Fatalf("POSIX drive-like URI = %q, err = %v", posixPath, err)
135		}
136	}
137	if runtime.GOOS == "windows" {
138		unc := `\\server\share\main.go`
139		uncURI, err := pathToURI(unc)
140		if err != nil {
141			t.Fatal(err)
142		}
143		if !strings.HasPrefix(uncURI, "file://server/") {
144			t.Fatalf("UNC URI = %q", uncURI)
145		}
146		uncPath, err := uriToPath(uncURI)
147		if err != nil || filepath.Clean(uncPath) != filepath.Clean(unc) {
148			t.Fatalf("UNC round trip = %q, err = %v", uncPath, err)
149		}
150	}
151}
152
153func TestFramingRejectsMalformedAndOversizedMessages(t *testing.T) {
154	cases := []string{
155		"\r\n{}",
156		"Content-Length: -1\r\n\r\n",
157		"Content-Length: nope\r\n\r\n",
158		"Content-Length: 1\r\n" + strings.Repeat("X", maxHeaderBytes) + "\r\n",
159		"Content-Length: " + strconv.Itoa(maxMessageBytes+1) + "\r\n\r\n",
160	}
161	for _, wire := range cases {
162		if _, err := ReadMessage(bufio.NewReader(strings.NewReader(wire))); err == nil {
163			t.Fatalf("accepted malformed frame %q", wire[:min(len(wire), 80)])
164		}
165	}
166	var out bytes.Buffer
167	if err := WriteMessage(&out, make([]byte, maxMessageBytes+1)); err == nil {
168		t.Fatal("accepted oversized outbound frame")
169	}
170}
171
172func TestFramingRoundTripWithAdditionalHeader(t *testing.T) {
173	payload := []byte(`{"jsonrpc":"2.0","id":7,"result":{"ok":true}}`)
174	wire := append([]byte("Content-Type: application/vscode-jsonrpc; charset=utf-8\r\n"), Frame(payload)...)
175	got, err := ReadMessage(bufio.NewReader(bytes.NewReader(wire)))
176	if err != nil {
177		t.Fatal(err)
178	}
179	if !bytes.Equal(got, payload) {
180		t.Fatalf("payload = %q, want %q", got, payload)
181	}
182}
183
184func TestZutHomeEnvironmentPrecedence(t *testing.T) {
185	configuredHome := filepath.Join(t.TempDir(), "zut")
186	xdgHome := filepath.Join(t.TempDir(), "state")
187	t.Setenv("ZUT_HOME", configuredHome)
188	t.Setenv("XDG_STATE_HOME", xdgHome)
189	if got := zutHome(); got != configuredHome {
190		t.Fatalf("ZUT_HOME path = %q, want %q", got, configuredHome)
191	}
192	t.Setenv("ZUT_HOME", "")
193	if got := zutHome(); got != filepath.Join(xdgHome, "zut") {
194		t.Fatalf("XDG_STATE_HOME path = %q, want %q", got, filepath.Join(xdgHome, "zut"))
195	}
196}
197
198func TestLoadConfigMergesGlobalProjectAndBuiltins(t *testing.T) {
199	root := t.TempDir()
200	zutHome := t.TempDir()
201	t.Setenv("ZUT_HOME", zutHome)
202	if err := os.WriteFile(filepath.Join(zutHome, "lsp.json"), []byte(`{"servers":{"gopls":{"settings":{"global":true},"args":["--global"]}}}`), 0o644); err != nil {
203		t.Fatal(err)
204	}
205	if err := os.WriteFile(filepath.Join(root, "go.mod"), []byte("module example\n"), 0o644); err != nil {
206		t.Fatal(err)
207	}
208	if err := os.WriteFile(filepath.Join(root, "lsp.json"), []byte(`{"gopls":{"command":"local-gopls","args":["--local"],"settings":{"local":true}},"sarif-lint":{"command":"reviewdog","kind":"cli","parser":"sarif","fileTypes":[".go"]}}`), 0o644); err != nil {
209		t.Fatal(err)
210	}
211	config, err := LoadConfig(root)
212	if err != nil {
213		t.Fatal(err)
214	}
215	gopls := config.Servers["gopls"]
216	if gopls.Command != "local-gopls" || len(gopls.Args) != 1 || gopls.Args[0] != "--local" {
217		t.Fatalf("gopls override = %#v", gopls)
218	}
219	if len(gopls.FileTypes) == 0 || len(gopls.RootMarkers) == 0 {
220		t.Fatalf("builtin selectors were not retained: %#v", gopls)
221	}
222	if gopls.Settings["global"] != true || gopls.Settings["local"] != true {
223		t.Fatalf("settings did not merge: %#v", gopls.Settings)
224	}
225	if config.Servers["sarif-lint"].Kind != "cli" || config.Servers["sarif-lint"].Parser != "sarif" {
226		t.Fatalf("custom linter = %#v", config.Servers["sarif-lint"])
227	}
228	selected := config.ApplicableServers(root, filepath.Join(root, "main.go"))
229	var foundGopls, foundSarif bool
230	for _, server := range selected {
231		foundGopls = foundGopls || server.ID == "gopls"
232		foundSarif = foundSarif || server.ID == "sarif-lint"
233	}
234	if !foundGopls || !foundSarif {
235		t.Fatalf("selected servers = %#v", selected)
236	}
237}
238
239func TestLoadConfigAcceptsProviderArrays(t *testing.T) {
240	root := t.TempDir()
241	zutHome := t.TempDir()
242	t.Setenv("ZUT_HOME", zutHome)
243	if err := os.WriteFile(filepath.Join(root, "go.mod"), []byte("module example\n"), 0o644); err != nil {
244		t.Fatal(err)
245	}
246	configFile := `{
247		"autoDetect": true,
248		"providers": [
249			"gopls",
250			{"id":"gopls","args":["serve"]},
251			{"id":"custom-lint","kind":"cli","command":"custom-lint","fileTypes":[".go"],"rootMarkers":["go.mod"],"cli":{"parser":"generic","mode":"files"}}
252		]
253	}`
254	if err := os.WriteFile(filepath.Join(root, "lsp.json"), []byte(configFile), 0o644); err != nil {
255		t.Fatal(err)
256	}
257	config, err := LoadConfig(root)
258	if err != nil {
259		t.Fatal(err)
260	}
261	if got := config.Servers["gopls"].Args; len(got) != 1 || got[0] != "serve" {
262		t.Fatalf("gopls provider override = %#v", got)
263	}
264	lint := config.Servers["custom-lint"]
265	if lint.Kind != "cli" || lint.Parser != "generic" || lint.Mode != "files" {
266		t.Fatalf("custom provider = %#v", lint)
267	}
268}
269
270func TestLoadConfigAutoDetectFalseKeepsExplicitProviders(t *testing.T) {
271	root := t.TempDir()
272	t.Setenv("ZUT_HOME", t.TempDir())
273	if err := os.WriteFile(filepath.Join(root, "go.mod"), []byte("module example\n"), 0o644); err != nil {
274		t.Fatal(err)
275	}
276	if err := os.WriteFile(filepath.Join(root, "lsp.json"), []byte(`{"autoDetect":false,"providers":["gopls"]}`), 0o644); err != nil {
277		t.Fatal(err)
278	}
279	config, err := LoadConfig(root)
280	if err != nil {
281		t.Fatal(err)
282	}
283	selected := config.ApplicableServers(root, filepath.Join(root, "main.go"))
284	if len(selected) != 1 || selected[0].ID != "gopls" {
285		t.Fatalf("explicit providers with autoDetect=false = %#v", selected)
286	}
287}
288
289func TestResolveCommandPrefersWorkspaceTools(t *testing.T) {
290	if runtime.GOOS == "windows" {
291		t.Skip("executable bits are not represented on Windows")
292	}
293	root := t.TempDir()
294	bin := filepath.Join(root, "node_modules", ".bin")
295	if err := os.MkdirAll(bin, 0o755); err != nil {
296		t.Fatal(err)
297	}
298	command := filepath.Join(bin, "fake-lsp")
299	if err := os.WriteFile(command, []byte("#!/bin/sh\n"), 0o755); err != nil {
300		t.Fatal(err)
301	}
302	got, err := ResolveCommand(root, "fake-lsp")
303	if err != nil {
304		t.Fatal(err)
305	}
306	if got != command {
307		t.Fatalf("resolved %q, want %q", got, command)
308	}
309}
310
311func TestParsers(t *testing.T) {
312	root := t.TempDir()
313	eslint := `[ {"filePath":"src/a.js","messages":[{"ruleId":"no-x","severity":2,"message":"bad","line":2,"column":3}] } ]`
314	got := ParseDiagnostics("eslint-json", "eslint", root, eslint)
315	if len(got) != 1 || got[0].Severity != "error" || got[0].Range.Start.Line != 1 {
316		t.Fatalf("eslint = %#v", got)
317	}
318	golangci := `{"Issues":[{"FromLinter":"govet","Text":"bad","Severity":"warning","Pos":{"Filename":"a.go","Line":3,"Column":4}}]}`
319	if got = ParseDiagnostics("golangci-lint-json", "golangci", root, golangci); len(got) != 1 || got[0].Severity != "warning" {
320		t.Fatalf("golangci = %#v", got)
321	}
322	ruff := `[{"code":"F401","message":"unused","filename":"a.py","location":{"row":1,"column":2}}]`
323	if got = ParseDiagnostics("ruff-json", "ruff", root, ruff); len(got) != 1 || got[0].Code != "F401" {
324		t.Fatalf("ruff = %#v", got)
325	}
326	sarif := `{"runs":[{"results":[{"ruleId":"R1","level":"error","message":{"text":"bad"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"a.c"},"region":{"startLine":4,"startColumn":2}}}]}]}]}`
327	if got = ParseDiagnostics("sarif", "scan", root, sarif); len(got) != 1 || got[0].Range.Start.Line != 3 {
328		t.Fatalf("sarif = %#v", got)
329	}
330	if got = ParseDiagnostics("generic", "lint", root, "a.go:5:6: warning: bad"); len(got) != 1 || got[0].Severity != "warning" {
331		t.Fatalf("generic = %#v", got)
332	}
333	if got = ParseDiagnostics("generic", "lint", root, "a.go:5:6: cannot find name"); len(got) != 1 || got[0].Message != "cannot find name" {
334		t.Fatalf("generic message = %#v", got)
335	}
336}
337
338func TestDiagnosticDedupUsesCodeAndStartPosition(t *testing.T) {
339	path := filepath.Join(t.TempDir(), "main.go")
340	base := Diagnostic{Path: path, Severity: "error", Code: "E1", Message: "same", Range: Range{Start: Position{Line: 1, Character: 2}, End: Position{Line: 1, Character: 3}}}
341	endChanged := base
342	endChanged.Range.End.Character = 20
343	otherCode := base
344	otherCode.Code = "E2"
345	otherStart := base
346	otherStart.Range.Start.Line++
347	if got := DeduplicateDiagnostics([]Diagnostic{base, endChanged, otherCode, otherStart}); len(got) != 3 {
348		t.Fatalf("deduplicated diagnostics = %#v, want three reports", got)
349	}
350}
351
352func TestManagerClearsPublishedDiagnostics(t *testing.T) {
353	manager := NewManager()
354	root := t.TempDir()
355	path := filepath.Join(root, "main.go")
356	if err := os.WriteFile(path, []byte("package main\n"), 0o644); err != nil {
357		t.Fatal(err)
358	}
359	ws, err := manager.workspace(root)
360	if err != nil {
361		t.Fatal(err)
362	}
363	manager.storeDiagnostics(ws, "fake", []Diagnostic{{Path: path, Message: "problem"}})
364	if got := manager.snapshot(ws, path); len(got) != 1 {
365		t.Fatalf("stored diagnostics = %#v", got)
366	}
367	manager.storeDiagnostics(ws, "fake", []Diagnostic{{Path: path, Clear: true}})
368	if got := manager.snapshot(ws, path); len(got) != 0 {
369		t.Fatalf("cleared diagnostics = %#v", got)
370	}
371}
372
373func TestDiagnosticsDedupAndGrouping(t *testing.T) {
374	root := t.TempDir()
375	makeDiag := func(path, message string, line int, server string) Diagnostic {
376		return Diagnostic{Path: filepath.Join(root, path), Severity: "error", Code: "E", Message: message, Server: server, Range: Range{Start: Position{Line: line}}}
377	}
378	input := []Diagnostic{makeDiag("a.go", "same", 0, "gopls"), makeDiag("a.go", "same", 0, "eslint"), makeDiag("a.go", `Cannot find module "missing"`, 1, "gopls"), makeDiag("a.go", "cascade one", 2, "gopls"), makeDiag("a.go", "cascade two", 3, "gopls"), makeDiag("b.go", "repeat", 1, "a"), makeDiag("c.go", "repeat", 2, "b"), makeDiag("d.go", "repeat", 3, "c")}
379	if got := DeduplicateDiagnostics(input); len(got) != len(input)-1 {
380		t.Fatalf("dedup count = %d", len(got))
381	}
382	text := SummarizeDiagnostics(input, root, 50)
383	if !strings.Contains(text, "repeated") || !strings.Contains(text, "SECONDARY") {
384		t.Fatalf("summary lacks grouping:\n%s", text)
385	}
386}
387
388func TestApplyWorkspaceEditUsesUTF16AndRejectsOutside(t *testing.T) {
389	root := t.TempDir()
390	path := filepath.Join(root, "a.txt")
391	if err := os.WriteFile(path, []byte("a😀b\n"), 0o644); err != nil {
392		t.Fatal(err)
393	}
394	uri := URIForPath(path)
395	ed := WorkspaceEdit{Changes: map[string][]TextEdit{uri: {{Range: Range{Start: Position{Line: 0, Character: 1}, End: Position{Line: 0, Character: 3}}, NewText: "X"}}}}
396	if err := ApplyWorkspaceEdit(root, ed); err != nil {
397		t.Fatal(err)
398	}
399	data, _ := os.ReadFile(path)
400	if string(data) != "aXb\n" {
401		t.Fatalf("edited data = %q", data)
402	}
403	outside := WorkspaceEdit{Changes: map[string][]TextEdit{URIForPath(filepath.Join(filepath.Dir(root), "outside.txt")): {{Range: Range{}, NewText: "x"}}}}
404	if err := ApplyWorkspaceEdit(root, outside); err == nil {
405		t.Fatal("outside edit was accepted")
406	}
407}
408
409func TestWorkspaceEditPreflightsAllTargets(t *testing.T) {
410	root := t.TempDir()
411	inside := filepath.Join(root, "inside.txt")
412	outside := filepath.Join(t.TempDir(), "outside.txt")
413	if err := os.WriteFile(inside, []byte("old\n"), 0o644); err != nil {
414		t.Fatal(err)
415	}
416	if err := os.WriteFile(outside, []byte("old\n"), 0o644); err != nil {
417		t.Fatal(err)
418	}
419	rootReal, err := filepath.EvalSymlinks(root)
420	if err != nil {
421		t.Fatal(err)
422	}
423	_, err = prepareEditTargets(rootReal, map[string][]TextEdit{
424		URIForPath(inside):  {{Range: Range{}, NewText: "inside"}},
425		URIForPath(outside): {{Range: Range{}, NewText: "outside"}},
426	}, nil)
427	if err == nil {
428		t.Fatal("workspace edit accepted an unsafe target after a safe target")
429	}
430}
431
432func TestApplyWorkspaceEditFollowsInWorkspaceSymlink(t *testing.T) {
433	if runtime.GOOS == "windows" {
434		t.Skip("symlink permissions vary on Windows")
435	}
436	root := t.TempDir()
437	target := filepath.Join(root, "target.txt")
438	link := filepath.Join(root, "link.txt")
439	if err := os.WriteFile(target, []byte("old\n"), 0o644); err != nil {
440		t.Fatal(err)
441	}
442	if err := os.Symlink(target, link); err != nil {
443		t.Fatal(err)
444	}
445	edit := WorkspaceEdit{Changes: map[string][]TextEdit{URIForPath(link): {{Range: Range{Start: Position{Line: 0, Character: 0}, End: Position{Line: 0, Character: 3}}, NewText: "new"}}}}
446	if err := ApplyWorkspaceEdit(root, edit); err != nil {
447		t.Fatal(err)
448	}
449	data, err := os.ReadFile(target)
450	if err != nil {
451		t.Fatal(err)
452	}
453	if string(data) != "new\n" {
454		t.Fatalf("target data = %q", data)
455	}
456	info, err := os.Lstat(link)
457	if err != nil {
458		t.Fatal(err)
459	}
460	if info.Mode()&os.ModeSymlink == 0 {
461		t.Fatal("workspace edit replaced the symlink")
462	}
463}
464
465func TestLimitedBufferCapsReaderCopy(t *testing.T) {
466	var buffer limitedBuffer
467	buffer.limit = 4
468	copied, err := io.Copy(&buffer, strings.NewReader("123456"))
469	if err != nil {
470		t.Fatal(err)
471	}
472	if copied != 6 || buffer.String() != "1234" {
473		t.Fatalf("copied %d bytes into %q", copied, buffer.String())
474	}
475}
476
477func TestDiagnosticJSONCodeNumber(t *testing.T) {
478	var diagnostic Diagnostic
479	if err := json.Unmarshal([]byte(`{"range":{"start":{"line":0,"character":0},"end":{"line":0,"character":1}},"severity":2,"code":123,"message":"warning"}`), &diagnostic); err != nil {
480		t.Fatal(err)
481	}
482	if diagnostic.Code != "123" || diagnostic.Severity != "warning" {
483		t.Fatalf("diagnostic = %#v", diagnostic)
484	}
485}
486
487func TestManagerClosePreventsNewWorkspaces(t *testing.T) {
488	manager := NewManager()
489	root := t.TempDir()
490	if err := manager.Close(); err != nil {
491		t.Fatal(err)
492	}
493	if err := manager.Close(); err != nil {
494		t.Fatal(err)
495	}
496	if _, err := manager.Config(root); err == nil {
497		t.Fatal("closed manager accepted a new workspace")
498	}
499	if err := manager.Reload(root); err == nil {
500		t.Fatal("closed manager accepted a reload")
501	}
502}
503
504func TestReduceDiagnosticsSuppressesLocationOnlyChanges(t *testing.T) {
505	manager := NewManager()
506	root := t.TempDir()
507	path := filepath.Join(root, "main.go")
508	first := Diagnostic{Path: path, Severity: "error", Message: "type mismatch", Range: Range{Start: Position{Line: 1, Character: 2}}}
509	if got := manager.ReduceDiagnostics(root, path, []Diagnostic{first}); len(got) != 1 {
510		t.Fatalf("first reduction returned %d diagnostics, want 1", len(got))
511	}
512	shifted := first
513	shifted.Range.Start.Line = 20
514	if got := manager.ReduceDiagnostics(root, path, []Diagnostic{shifted}); len(got) != 0 {
515		t.Fatalf("shifted diagnostic was replayed: %#v", got)
516	}
517	if got := manager.ReduceDiagnostics(root, path, nil); len(got) != 0 {
518		t.Fatalf("clear returned %#v", got)
519	}
520	if got := manager.ReduceDiagnostics(root, path, []Diagnostic{shifted}); len(got) != 1 {
521		t.Fatalf("diagnostic did not reappear after clear: %#v", got)
522	}
523}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 477

↑↓ moveenter openesc close