v0.29.0 73d6864

protocol.go RawHistoryFolder

1package lsp
2
3import (
4	"encoding/json"
5	"fmt"
6	"net/url"
7	"path/filepath"
8	"runtime"
9	"strings"
10)
11
12// Position and Range use the zero-based line and UTF-16 character indexing
13// defined by LSP.
14type Position struct {
15	Line      int `json:"line"`
16	Character int `json:"character"`
17}
18
19type Range struct {
20	Start Position `json:"start"`
21	End   Position `json:"end"`
22}
23
24type TextEdit struct {
25	Range   Range  `json:"range"`
26	NewText string `json:"newText"`
27}
28
29// WorkspaceEdit is the safe subset of a workspace edit understood by zut.
30// Resource operations in DocumentChanges are deliberately rejected.
31type WorkspaceEdit struct {
32	Changes           map[string][]TextEdit `json:"changes,omitempty"`
33	DocumentChanges   []json.RawMessage     `json:"documentChanges,omitempty"`
34	ChangeAnnotations json.RawMessage       `json:"changeAnnotations,omitempty"`
35}
36
37// Diagnostic is the normalized diagnostic representation shared by LSP and
38// CLI linters. Path is absolute when it came from a workspace-backed source.
39type Diagnostic struct {
40	Path     string `json:"path"`
41	Range    Range  `json:"range"`
42	Severity string `json:"severity"`
43	Code     string `json:"code,omitempty"`
44	Message  string `json:"message"`
45	Source   string `json:"source,omitempty"`
46	Server   string `json:"server,omitempty"`
47	Clear    bool   `json:"-"`
48}
49
50func (d *Diagnostic) UnmarshalJSON(data []byte) error {
51	var raw struct {
52		Range    Range           `json:"range"`
53		Severity json.RawMessage `json:"severity"`
54		Code     json.RawMessage `json:"code"`
55		Message  string          `json:"message"`
56		Source   string          `json:"source"`
57	}
58	if err := json.Unmarshal(data, &raw); err != nil {
59		return err
60	}
61	d.Range, d.Message, d.Source = raw.Range, raw.Message, raw.Source
62	d.Severity = severityFromJSON(raw.Severity)
63	d.Code = stringFromJSON(raw.Code)
64	return nil
65}
66
67func severityFromJSON(raw json.RawMessage) string {
68	var n int
69	if json.Unmarshal(raw, &n) == nil {
70		switch n {
71		case 1:
72			return "error"
73		case 2:
74			return "warning"
75		case 3:
76			return "info"
77		case 4:
78			return "hint"
79		}
80	}
81	var s string
82	if json.Unmarshal(raw, &s) == nil {
83		switch strings.ToLower(s) {
84		case "error", "err", "fatal":
85			return "error"
86		case "warning", "warn":
87			return "warning"
88		case "hint":
89			return "hint"
90		default:
91			return "info"
92		}
93	}
94	return "info"
95}
96
97func stringFromJSON(raw json.RawMessage) string {
98	if len(raw) == 0 || string(raw) == "null" {
99		return ""
100	}
101	var s string
102	if json.Unmarshal(raw, &s) == nil {
103		return s
104	}
105	var n json.Number
106	if json.Unmarshal(raw, &n) == nil {
107		return n.String()
108	}
109	return strings.Trim(string(raw), `"`)
110}
111
112// URIForPath returns the escaped file URI used by LSP document params.
113func URIForPath(path string) string {
114	uri, _ := pathToURI(path)
115	return uri
116}
117
118func pathToURI(path string) (string, error) {
119	abs, err := filepath.Abs(path)
120	if err != nil {
121		return "", err
122	}
123	// url.URL escapes spaces and non-ASCII while retaining path separators.
124	uriPath := filepath.ToSlash(abs)
125	if runtime.GOOS == "windows" && strings.HasPrefix(uriPath, "//") {
126		unc := strings.TrimPrefix(uriPath, "//")
127		if host, rest, ok := strings.Cut(unc, "/"); ok {
128			return (&url.URL{Scheme: "file", Host: host, Path: "/" + rest}).String(), nil
129		}
130	}
131	if !strings.HasPrefix(uriPath, "/") {
132		uriPath = "/" + uriPath
133	}
134	u := &url.URL{Scheme: "file", Path: uriPath}
135	return u.String(), nil
136}
137
138func uriToPath(uri string) (string, error) {
139	u, err := url.Parse(uri)
140	if err != nil {
141		return "", err
142	}
143	if u.Scheme != "" && !strings.EqualFold(u.Scheme, "file") {
144		return "", fmt.Errorf("unsupported document URI scheme %q", u.Scheme)
145	}
146	path := u.Path
147	if u.Host != "" && u.Host != "localhost" {
148		path = "//" + u.Host + path
149	}
150	if path == "" {
151		return "", fmt.Errorf("document URI has no path")
152	}
153	if runtime.GOOS == "windows" && len(path) >= 3 && path[0] == '/' && path[2] == ':' { // file:///C:/...
154		path = path[1:]
155	}
156	return filepath.Clean(filepath.FromSlash(path)), nil
157}

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 476

↑↓ moveenter openesc close