1package main
2
3import (
4	"bytes"
5	"errors"
6	"fmt"
7	"io/ioutil"
8	"os"
9	"path/filepath"
10	"strconv"
11	"strings"
12
13	"github.com/xyproto/env"
14)
15
16const maxLocationHistoryEntries = 1024
17
18var (
19	vimLocationHistoryFilename   = env.ExpandUser("~/.viminfo")
20	emacsLocationHistoryFilename = env.ExpandUser("~/.emacs.d/places")
21	userCacheDir                 = env.Dir("XDG_CACHE_HOME", "~/.cache")
22	locationHistoryFilename      = filepath.Join(userCacheDir, "o/locations.txt")
23	nvimLocationHistoryFilename  = filepath.Join(env.Dir("XDG_DATA_HOME", "~/.local/share"), "nvim/shada/main.shada")
24)
25
26// LoadLocationHistory will attempt to load the per-absolute-filename recording of which line is active.
27// The returned map can be empty.
28func LoadLocationHistory(configFile string) (map[string]LineNumber, error) {
29	locationHistory := make(map[string]LineNumber)
30
31	contents, err := ioutil.ReadFile(configFile)
32	if err != nil {
33		// Could not read file, return an empty map and an error
34		return locationHistory, err
35	}
36	// The format of the file is, per line:
37	// "filename":location
38	for _, filenameLocation := range strings.Split(string(contents), "\n") {
39		if !strings.Contains(filenameLocation, ":") {
40			continue
41		}
42		fields := strings.SplitN(filenameLocation, ":", 2)
43
44		// Retrieve an unquoted filename in the filename variable
45		quotedFilename := strings.TrimSpace(fields[0])
46		filename := quotedFilename
47		if strings.HasPrefix(quotedFilename, "\"") && strings.HasSuffix(quotedFilename, "\"") {
48			filename = quotedFilename[1 : len(quotedFilename)-1]
49		}
50		if filename == "" {
51			continue
52		}
53
54		// Retrieve the line number
55		lineNumberString := strings.TrimSpace(fields[1])
56		lineNumber, err := strconv.Atoi(lineNumberString)
57		if err != nil {
58			// Could not convert to a number
59			continue
60		}
61		locationHistory[filename] = LineNumber(lineNumber)
62	}
63
64	// Return the location history map. It could be empty, which is fine.
65	return locationHistory, nil
66}
67
68// LoadVimLocationHistory will attempt to load the history of where the cursor should be when opening a file from ~/.viminfo
69// The returned map can be empty. The filenames have absolute paths.
70func LoadVimLocationHistory(vimInfoFilename string) map[string]LineNumber {
71	locationHistory := make(map[string]LineNumber)
72	// Attempt to read the ViM location history (that may or may not exist)
73	data, err := ioutil.ReadFile(vimInfoFilename)
74	if err != nil {
75		return locationHistory
76	}
77	for _, line := range strings.Split(string(data), "\n") {
78		if strings.HasPrefix(line, "-'") {
79			fields := strings.Fields(line)
80			if len(fields) < 4 {
81				continue
82			}
83			lineNumberString := fields[1]
84			//colNumberString := fields[2]
85			filename := fields[3]
86			// Skip if the filename already exists in the location history, since .viminfo
87			// may have duplication locations and lists the newest first.
88			if _, alreadyExists := locationHistory[filename]; alreadyExists {
89				continue
90			}
91			lineNumber, err := strconv.Atoi(lineNumberString)
92			if err != nil {
93				// Not a line number after all
94				continue
95			}
96			absFilename, err := filepath.Abs(filename)
97			if err != nil {
98				// Could not get the absolute path
99				continue
100			}
101			absFilename = filepath.Clean(absFilename)
102			locationHistory[absFilename] = LineNumber(lineNumber)
103		}
104	}
105	return locationHistory
106}
107
108// FindInVimLocationHistory will try to find the given filename in the ViM .viminfo file
109func FindInVimLocationHistory(vimInfoFilename, searchFilename string) (LineNumber, error) {
110	// Attempt to read the ViM location history (that may or may not exist)
111	data, err := ioutil.ReadFile(vimInfoFilename)
112	if err != nil {
113		return LineNumber(-1), err
114	}
115	for _, line := range strings.Split(string(data), "\n") {
116		if strings.HasPrefix(line, "-'") {
117			fields := strings.Fields(line)
118			if len(fields) < 4 {
119				continue
120			}
121			lineNumberString := fields[1]
122			filename := fields[3]
123			lineNumber, err := strconv.Atoi(lineNumberString)
124			if err != nil {
125				// Not a line number after all
126				continue
127			}
128			absFilename, err := filepath.Abs(filename)
129			if err != nil {
130				// Could not get the absolute path
131				continue
132			}
133			absFilename = filepath.Clean(absFilename)
134			if absFilename == searchFilename {
135				return LineNumber(lineNumber), nil
136			}
137		}
138	}
139	return LineNumber(-1), errors.New("filename not found in vim location history: " + searchFilename)
140}
141
142// FindInNvimLocationHistory will try to find the given filename in the NeoVim location history file
143func FindInNvimLocationHistory(nvimLocationFilename, searchFilename string) (LineNumber, error) {
144	nol := LineNumber(-1) // no line number
145
146	data, err := ioutil.ReadFile(nvimLocationFilename) // typically main.shada, a MsgPack file
147	if err != nil {
148		return nol, err
149	}
150
151	pos := bytes.Index(data, []byte(searchFilename))
152
153	if pos < 0 {
154		return nol, errors.New("filename not found in nvim location history: " + searchFilename)
155	}
156
157	if pos < 2 {
158		// this should never happen
159		return nol, errors.New("too early match in file")
160	}
161
162	dataType := data[pos-2]
163	if dataType != 0xc4 {
164		// this should never happen
165		return nol, errors.New("not a binary data type")
166	}
167
168	maxi := len(data) - 1
169	nextNumberIsTheLineNumber := false
170
171	pp := pos - 2
172
173	// Search 512 bytes from here, at a maximum
174	for i := 0; i < 512; i++ {
175		if (pp + i) >= maxi {
176			return nol, errors.New("corresponding line not found for " + searchFilename)
177		}
178		b := data[pp+i] // The current byte
179
180		//fmt.Printf("--- byte pos %d [value %x]---\n", i, b)
181
182		switch {
183		case b <= 0x7f: // fixint
184			//fmt.Printf("%d (positive fixint)\n", b)
185			if nextNumberIsTheLineNumber {
186				return LineNumber(int(b)), nil
187			}
188		case b >= 0x80 && b <= 0x8f: // fixmap
189			size := uint(b - 128) // - b10000000
190			size--
191			size *= 2
192			bd := data[pp+i+1 : pp+i+1+int(size)]
193			i += int(size)
194			_ = bd
195			//fmt.Printf("%s (fixmap, size %d)\n", bd, size)
196		case b >= 0x90 && b <= 0x9f: // fixarray
197			size := uint(b - 144) // - b10010000
198			size--
199			bd := data[pp+i+1 : pp+i+1+int(size)]
200			i += int(size)
201			_ = bd
202			//fmt.Printf("%s (fixarray, size %d)\n", bd, size)
203		case b >= 0xa0 && b <= 0xbf: // fixstr
204			size := uint(b - 160) // - 101xxxxx
205			bd := data[pp+i+1 : pp+i+1+int(size)]
206			i += int(size)
207			//fmt.Printf("%s (fixstr, size %d)\n", string(bd), size)
208			if string(bd) == "l" {
209				// Found a NeoVim line number string "l", specifying that the next
210				// element is a number.
211				nextNumberIsTheLineNumber = true
212			}
213		case b == 0xc0: // nil
214			//fmt.Println("nil")
215		case b == 0xc1: // unused
216			//fmt.Println("<unused>")
217		case b == 0xc2: // false
218			//fmt.Println("false")
219		case b == 0xc3: // true
220			//fmt.Println("true")
221		case b == 0xc4: // bin 8
222			i++
223			size := uint(data[pp+i])
224			bd := data[pp+i+1 : pp+i+1+int(size)]
225			i += int(size)
226			_ = bd
227			//fmt.Printf("%s (bin 8, size %d)\n", string(bd), size)
228		case b == 0xc5:
229			//fmt.Println("bin 16")
230			return nol, errors.New("unimplemented msgpack field: bin 16")
231		case b == 0xc6:
232			//fmt.Println("bin 32")
233			return nol, errors.New("unimplemented msgpack field: bin 32")
234		case b == 0xc7:
235			//fmt.Println("ext 8")
236			return nol, errors.New("unimplemented msgpack field: ext 8")
237		case b == 0xc8:
238			//fmt.Println("ext 16")
239			return nol, errors.New("unimplemented msgpack field: ext 16")
240		case b == 0xc9:
241			//fmt.Println("ext 32")
242			return nol, errors.New("unimplemented msgpack field: ext 32")
243		case b == 0xca:
244			//fmt.Println("float 32")
245			return nol, errors.New("unimplemented msgpack field: float 32")
246		case b == 0xcb:
247			//fmt.Println("float 64")
248			return nol, errors.New("unimplemented msgpack field: float 64")
249		case b == 0xcc: // uint 8
250			i++
251			d0 := data[pp+i]
252			l := d0
253			//fmt.Printf("%d (uint 8)\n", l)
254			if nextNumberIsTheLineNumber {
255				return LineNumber(l), nil
256			}
257		case b == 0xcd: // uint 16
258			i++
259			d0 := data[pp+i]
260			i++
261			d1 := data[pp+i]
262			l := uint16(d0)<<8 + uint16(d1)
263			//fmt.Printf("%d (uint 16)\n", l)
264			if nextNumberIsTheLineNumber {
265				return LineNumber(l), nil
266			}
267		case b == 0xce: // uint 32
268			i++
269			d0 := data[pp+i]
270			i++
271			d1 := data[pp+i]
272			i++
273			d2 := data[pp+i]
274			i++
275			d3 := data[pp+i]
276			l := uint32(d0)<<24 + uint32(d1)<<16 + uint32(d2)<<8 + uint32(d3)
277			//fmt.Printf("%d (uint 32)\n", l)
278			if nextNumberIsTheLineNumber {
279				return LineNumber(l), nil
280			}
281		case b == 0xcf:
282			i++
283			d0 := data[pp+i]
284			i++
285			d1 := data[pp+i]
286			i++
287			d2 := data[pp+i]
288			i++
289			d3 := data[pp+i]
290			i++
291			d4 := data[pp+i]
292			i++
293			d5 := data[pp+i]
294			i++
295			d6 := data[pp+i]
296			i++
297			d7 := data[pp+i]
298			l := uint64(d0)<<56 + uint64(d1)<<48 + uint64(d2)<<40 + uint64(d3)<<32 + uint64(d4)<<24 + uint64(d5)<<16 + uint64(d6)<<8 + uint64(d7)
299			//fmt.Printf("%d (uint 64)\n", l)
300			if nextNumberIsTheLineNumber {
301				return LineNumber(l), nil
302			}
303		case b == 0xd0:
304			//fmt.Println("int 8")
305			return nol, errors.New("unimplemented msgpack field: int 8")
306		case b == 0xd1:
307			//fmt.Println("int 16")
308			return nol, errors.New("unimplemented msgpack field: int 16")
309		case b == 0xd2:
310			//fmt.Println("int 32")
311			return nol, errors.New("unimplemented msgpack field: int 32")
312		case b == 0xd3:
313			//fmt.Println("int 64")
314			return nol, errors.New("unimplemented msgpack field: int 64")
315		case b == 0xd4:
316			//fmt.Println("fixext 1")
317			return nol, errors.New("unimplemented msgpack field: fixext 1")
318		case b == 0xd5:
319			//fmt.Println("fixext 2")
320			return nol, errors.New("unimplemented msgpack field: fixext 2")
321		case b == 0xd6:
322			//fmt.Println("fixext 4")
323			return nol, errors.New("unimplemented msgpack field: fixext 4")
324		case b == 0xd7:
325			//fmt.Println("fixext 8")
326			return nol, errors.New("unimplemented msgpack field: fixext 8")
327		case b == 0xd8:
328			//fmt.Println("fixext 16")
329			return nol, errors.New("unimplemented msgpack field: fixext 16")
330		case b == 0xd9:
331			//fmt.Println("str 8")
332			return nol, errors.New("unimplemented msgpack field: str 8")
333		case b == 0xda:
334			//fmt.Println("str 16")
335			return nol, errors.New("unimplemented msgpack field: str 16")
336		case b == 0xdb:
337			//fmt.Println("str 32")
338			return nol, errors.New("unimplemented msgpack field: str 32")
339		case b == 0xdc:
340			//fmt.Println("array 16")
341			return nol, errors.New("unimplemented msgpack field: array 16")
342		case b == 0xdd:
343			//fmt.Println("array 32")
344			return nol, errors.New("unimplemented msgpack field: array 32")
345		case b == 0xde:
346			//fmt.Println("map 16")
347			return nol, errors.New("unimplemented msgpack field: map 16")
348		case b == 0xdf:
349			//fmt.Println("map 32")
350			return nol, errors.New("unimplemented msgpack field: map 32")
351		case b >= 0xe0 && b <= 0xff: // negative fixint
352			n := -(int(b) - 224) // - 111xxxxx
353			_ = n
354			//fmt.Printf("%d (negative fixint)\n", n)
355		default:
356			return nol, fmt.Errorf("unrecognized msgpack field: %x", b)
357		}
358	}
359	return nol, errors.New("could not find line number for " + searchFilename)
360}
361
362// LoadEmacsLocationHistory will attempt to load the history of where the cursor should be when opening a file from ~/.emacs.d/places.
363// The returned map can be empty. The filenames have absolute paths.
364// The values in the map are NOT line numbers but character positions.
365func LoadEmacsLocationHistory(emacsPlacesFilename string) map[string]CharacterPosition {
366	locationHistory := make(map[string]CharacterPosition)
367	// Attempt to read the Emacs location history (that may or may not exist)
368	data, err := ioutil.ReadFile(emacsPlacesFilename)
369	if err != nil {
370		return locationHistory
371	}
372	for _, line := range strings.Split(string(data), "\n") {
373		// Looking for lines with filenames with ""
374		fields := strings.SplitN(line, "\"", 3)
375		if len(fields) != 3 {
376			continue
377		}
378		filename := fields[1]
379		locationAndMore := fields[2]
380		// Strip trailing parenthesis
381		for strings.HasSuffix(locationAndMore, ")") {
382			locationAndMore = locationAndMore[:len(locationAndMore)-1]
383		}
384		fields = strings.Fields(locationAndMore)
385		if len(fields) == 0 {
386			continue
387		}
388		lastField := fields[len(fields)-1]
389		charNumber, err := strconv.Atoi(lastField)
390		if err != nil {
391			// Not a character number
392			continue
393		}
394		absFilename, err := filepath.Abs(filename)
395		if err != nil {
396			// Could not get absolute path
397			continue
398		}
399		absFilename = filepath.Clean(absFilename)
400		locationHistory[absFilename] = CharacterPosition(charNumber)
401	}
402	return locationHistory
403}
404
405// SaveLocationHistory will attempt to save the per-absolute-filename recording of which line is active
406func SaveLocationHistory(locationHistory map[string]LineNumber, configFile string) error {
407	// First create the folder, if needed, in a best effort attempt
408	folderPath := filepath.Dir(configFile)
409	os.MkdirAll(folderPath, os.ModePerm)
410
411	var sb strings.Builder
412	for k, v := range locationHistory {
413		sb.WriteString(fmt.Sprintf("\"%s\": %d\n", k, v))
414	}
415
416	// Write the location history and return the error, if any.
417	// The permissions are a bit stricter for this one.
418	return ioutil.WriteFile(configFile, []byte(sb.String()), 0600)
419}
420
421// SaveLocation takes a filename (which includes the absolute path) and a map which contains
422// an overview of which files were at which line location.
423func (e *Editor) SaveLocation(absFilename string, locationHistory map[string]LineNumber) error {
424	if len(locationHistory) > maxLocationHistoryEntries {
425		// Cull the history
426		locationHistory = make(map[string]LineNumber, 1)
427	}
428	// Save the current line location
429	locationHistory[absFilename] = e.LineNumber()
430	// Save the location history and return the error, if any
431	return SaveLocationHistory(locationHistory, locationHistoryFilename)
432}
433