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