123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723 |
- // License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
- package hints
- import (
- "bytes"
- "encoding/json"
- "errors"
- "fmt"
- "os/exec"
- "path/filepath"
- "regexp"
- "slices"
- "strconv"
- "strings"
- "sync"
- "unicode"
- "unicode/utf8"
- "github.com/dlclark/regexp2"
- "github.com/seancfoley/ipaddress-go/ipaddr"
- "kitty"
- "kitty/tools/config"
- "kitty/tools/tty"
- "kitty/tools/utils"
- )
- var _ = fmt.Print
- const (
- DEFAULT_HINT_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz"
- FILE_EXTENSION = `\.(?:[a-zA-Z0-9]{2,7}|[ahcmo])(?:\b|[^.])`
- )
- func path_regex() string {
- return fmt.Sprintf(`(?:\S*?/[\r\S]+)|(?:\S[\r\S]*%s)\b`, FILE_EXTENSION)
- }
- func default_linenum_regex() string {
- return fmt.Sprintf(`(?P<path>%s):(?P<line>\d+)`, path_regex())
- }
- type Mark struct {
- Index int `json:"index"`
- Start int `json:"start"`
- End int `json:"end"`
- Text string `json:"text"`
- Group_id string `json:"group_id"`
- Is_hyperlink bool `json:"is_hyperlink"`
- Groupdict map[string]any `json:"groupdict"`
- }
- func process_escape_codes(text string) (ans string, hyperlinks []Mark) {
- removed_size, idx := 0, 0
- active_hyperlink_url := ""
- active_hyperlink_id := ""
- active_hyperlink_start_offset := 0
- add_hyperlink := func(end int) {
- hyperlinks = append(hyperlinks, Mark{
- Index: idx, Start: active_hyperlink_start_offset, End: end, Text: active_hyperlink_url, Is_hyperlink: true, Group_id: active_hyperlink_id})
- active_hyperlink_url, active_hyperlink_id = "", ""
- active_hyperlink_start_offset = 0
- idx++
- }
- ans = utils.ReplaceAll(utils.MustCompile("\x1b(?:\\[[0-9;:]*?m|\\].*?\x1b\\\\)"), text, func(raw string, groupdict map[string]utils.SubMatch) string {
- if !strings.HasPrefix(raw, "\x1b]8") {
- removed_size += len(raw)
- return ""
- }
- start := groupdict[""].Start - removed_size
- removed_size += len(raw)
- if active_hyperlink_url != "" {
- add_hyperlink(start)
- }
- raw = raw[4 : len(raw)-2]
- if metadata, url, found := strings.Cut(raw, ";"); found && url != "" {
- active_hyperlink_url = url
- active_hyperlink_start_offset = start
- if metadata != "" {
- for _, entry := range strings.Split(metadata, ":") {
- if strings.HasPrefix(entry, "id=") && len(entry) > 3 {
- active_hyperlink_id = entry[3:]
- }
- }
- }
- }
- return ""
- })
- if active_hyperlink_url != "" {
- add_hyperlink(len(ans))
- }
- return
- }
- type PostProcessorFunc = func(string, int, int) (int, int)
- type GroupProcessorFunc = func(map[string]string)
- func is_punctuation(b string) bool {
- switch b {
- case ",", ".", "?", "!":
- return true
- }
- return false
- }
- func closing_bracket_for(ch string) string {
- switch ch {
- case "(":
- return ")"
- case "[":
- return "]"
- case "{":
- return "}"
- case "<":
- return ">"
- case "*":
- return "*"
- case `"`:
- return `"`
- case "'":
- return "'"
- case "“":
- return "”"
- case "‘":
- return "’"
- }
- return ""
- }
- func char_at(s string, i int) string {
- ans, _ := utf8.DecodeRuneInString(s[i:])
- if ans == utf8.RuneError {
- return ""
- }
- return string(ans)
- }
- func matching_remover(openers ...string) PostProcessorFunc {
- return func(text string, s, e int) (int, int) {
- if s < e && e <= len(text) {
- before := char_at(text, s)
- if slices.Index(openers, before) > -1 {
- q := closing_bracket_for(before)
- if e > 0 && char_at(text, e-1) == q {
- s++
- e--
- } else if char_at(text, e) == q {
- s++
- }
- }
- }
- return s, e
- }
- }
- func linenum_group_processor(gd map[string]string) {
- pat := utils.MustCompile(`:\d+$`)
- gd[`path`] = pat.ReplaceAllStringFunc(gd["path"], func(m string) string {
- gd["line"] = m[1:]
- return ``
- })
- gd[`path`] = utils.Expanduser(gd[`path`])
- }
- var PostProcessorMap = sync.OnceValue(func() map[string]PostProcessorFunc {
- return map[string]PostProcessorFunc{
- "url": func(text string, s, e int) (int, int) {
- if s > 4 && text[s-5:s] == "link:" { // asciidoc URLs
- url := text[s:e]
- idx := strings.LastIndex(url, "[")
- if idx > -1 {
- e -= len(url) - idx
- }
- }
- for e > 1 && is_punctuation(char_at(text, e)) { // remove trailing punctuation
- e--
- }
- // truncate url at closing bracket/quote
- if s > 0 && e <= len(text) && closing_bracket_for(char_at(text, s-1)) != "" {
- q := closing_bracket_for(char_at(text, s-1))
- idx := strings.Index(text[s:], q)
- if idx > 0 {
- e = s + idx
- }
- }
- // reStructuredText URLs
- if e > 3 && text[e-2:e] == "`_" {
- e -= 2
- }
- return s, e
- },
- "brackets": matching_remover("(", "{", "[", "<"),
- "quotes": matching_remover("'", `"`, "“", "‘"),
- "ip": func(text string, s, e int) (int, int) {
- addr := ipaddr.NewHostName(text[s:e])
- if !addr.IsAddress() {
- return -1, -1
- }
- return s, e
- },
- }
- })
- type KittyOpts struct {
- Url_prefixes *utils.Set[string]
- Url_excluded_characters string
- Select_by_word_characters string
- }
- func read_relevant_kitty_opts(path string) KittyOpts {
- ans := KittyOpts{
- Select_by_word_characters: kitty.KittyConfigDefaults.Select_by_word_characters,
- Url_excluded_characters: kitty.KittyConfigDefaults.Url_excluded_characters}
- handle_line := func(key, val string) error {
- switch key {
- case "url_prefixes":
- ans.Url_prefixes = utils.NewSetWithItems(strings.Split(val, " ")...)
- case "select_by_word_characters":
- ans.Select_by_word_characters = strings.TrimSpace(val)
- case "url_excluded_characters":
- if s, err := config.StringLiteral(val); err == nil {
- ans.Url_excluded_characters = s
- }
- }
- return nil
- }
- cp := config.ConfigParser{LineHandler: handle_line}
- _ = cp.ParseFiles(path) // ignore errors and use defaults
- if ans.Url_prefixes == nil {
- ans.Url_prefixes = utils.NewSetWithItems(kitty.KittyConfigDefaults.Url_prefixes...)
- }
- return ans
- }
- var RelevantKittyOpts = sync.OnceValue(func() KittyOpts {
- return read_relevant_kitty_opts(filepath.Join(utils.ConfigDir(), "kitty.conf"))
- })
- var debugprintln = tty.DebugPrintln
- var _ = debugprintln
- func url_excluded_characters_as_ranges_for_regex(extra_excluded string) string {
- // See https://url.spec.whatwg.org/#url-code-points
- ans := strings.Builder{}
- ans.Grow(4096)
- type cr struct{ start, end rune }
- ranges := []cr{}
- r := func(start rune, end ...rune) {
- if len(end) == 0 {
- ranges = append(ranges, cr{start, start})
- } else {
- ranges = append(ranges, cr{start, end[0]})
- }
- }
- if !strings.Contains(extra_excluded, "\n") {
- r('\n')
- }
- if !strings.Contains(extra_excluded, "\r") {
- r('\r')
- }
- r('!')
- r('$')
- r('&')
- r('#')
- r('\'')
- r('/')
- r(':')
- r(';')
- r('@')
- r('_')
- r('~')
- r('(')
- r(')')
- r('*')
- r('+')
- r(',')
- r('-')
- r('.')
- r('=')
- r('?')
- r('%')
- r('a', 'z')
- r('A', 'Z')
- r('0', '9')
- slices.SortFunc(ranges, func(a, b cr) int { return int(a.start - b.start) })
- var prev rune = -1
- for _, cr := range ranges {
- if cr.start-1 > prev+1 {
- ans.WriteString(regexp.QuoteMeta(string(prev + 1)))
- ans.WriteRune('-')
- ans.WriteString(regexp.QuoteMeta(string(cr.start - 1)))
- }
- prev = cr.end
- }
- ans.WriteString(regexp.QuoteMeta(string(ranges[len(ranges)-1].end + 1)))
- ans.WriteRune('-')
- ans.WriteRune(0x9f)
- ans.WriteString(`\x{d800}-\x{dfff}`)
- ans.WriteString(`\x{fdd0}-\x{fdef}`)
- w := func(x rune) { ans.WriteRune(x) }
- w(0xFFFE)
- w(0xFFFF)
- w(0x1FFFE)
- w(0x1FFFF)
- w(0x2FFFE)
- w(0x2FFFF)
- w(0x3FFFE)
- w(0x3FFFF)
- w(0x4FFFE)
- w(0x4FFFF)
- w(0x5FFFE)
- w(0x5FFFF)
- w(0x6FFFE)
- w(0x6FFFF)
- w(0x7FFFE)
- w(0x7FFFF)
- w(0x8FFFE)
- w(0x8FFFF)
- w(0x9FFFE)
- w(0x9FFFF)
- w(0xAFFFE)
- w(0xAFFFF)
- w(0xBFFFE)
- w(0xBFFFF)
- w(0xCFFFE)
- w(0xCFFFF)
- w(0xDFFFE)
- w(0xDFFFF)
- w(0xEFFFE)
- w(0xEFFFF)
- w(0xFFFFE)
- w(0xFFFFF)
- if strings.Contains(extra_excluded, "-") {
- extra_excluded = strings.ReplaceAll(extra_excluded, "-", "")
- extra_excluded = regexp.QuoteMeta(extra_excluded) + "-"
- } else {
- extra_excluded = regexp.QuoteMeta(extra_excluded)
- }
- ans.WriteString(extra_excluded)
- return ans.String()
- }
- func functions_for(opts *Options) (pattern string, post_processors []PostProcessorFunc, group_processors []GroupProcessorFunc, err error) {
- switch opts.Type {
- case "url":
- var url_prefixes *utils.Set[string]
- if opts.UrlPrefixes == "default" {
- url_prefixes = RelevantKittyOpts().Url_prefixes
- } else {
- url_prefixes = utils.NewSetWithItems(strings.Split(opts.UrlPrefixes, ",")...)
- }
- url_excluded_characters := RelevantKittyOpts().Url_excluded_characters
- if opts.UrlExcludedCharacters != "default" {
- if url_excluded_characters, err = config.StringLiteral(opts.UrlExcludedCharacters); err != nil {
- err = fmt.Errorf("Failed to parse --url-excluded-characters value: %#v with error: %w", opts.UrlExcludedCharacters, err)
- return
- }
- }
- pattern = fmt.Sprintf(`(?:%s)://[^%s]{3,}`, strings.Join(url_prefixes.AsSlice(), "|"), url_excluded_characters_as_ranges_for_regex(url_excluded_characters))
- post_processors = append(post_processors, PostProcessorMap()["url"])
- case "path":
- pattern = path_regex()
- post_processors = append(post_processors, PostProcessorMap()["brackets"], PostProcessorMap()["quotes"])
- case "line":
- pattern = "(?m)^\\s*(.+)[\\s\x00]*$"
- case "hash":
- pattern = "[0-9a-f][0-9a-f\r]{6,127}"
- case "ip":
- pattern = (
- // IPv4 with no validation
- `((?:\d{1,3}\.){3}\d{1,3}` + "|" +
- // IPv6 with no validation
- `(?:[a-fA-F0-9]{0,4}:){2,7}[a-fA-F0-9]{1,4})`)
- post_processors = append(post_processors, PostProcessorMap()["ip"])
- default:
- pattern = opts.Regex
- if opts.Type == "linenum" {
- if pattern == kitty.HintsDefaultRegex {
- pattern = default_linenum_regex()
- }
- post_processors = append(post_processors, PostProcessorMap()["brackets"], PostProcessorMap()["quotes"])
- group_processors = append(group_processors, linenum_group_processor)
- }
- }
- return
- }
- type Capture struct {
- Text string
- Text_as_runes []rune
- Byte_Offsets struct {
- Start, End int
- }
- Rune_Offsets struct {
- Start, End int
- }
- }
- func (self Capture) String() string {
- return fmt.Sprintf("Capture(start=%d, end=%d, %#v)", self.Byte_Offsets.Start, self.Byte_Offsets.End, self.Text)
- }
- type Group struct {
- Name string
- IsNamed bool
- Captures []Capture
- }
- func (self Group) LastCapture() Capture {
- if len(self.Captures) == 0 {
- return Capture{}
- }
- return self.Captures[len(self.Captures)-1]
- }
- func (self Group) String() string {
- return fmt.Sprintf("Group(name=%#v, captures=%v)", self.Name, self.Captures)
- }
- type Match struct {
- Groups []Group
- }
- func (self Match) HasNamedGroups() bool {
- for _, g := range self.Groups {
- if g.IsNamed {
- return true
- }
- }
- return false
- }
- func find_all_matches(re *regexp2.Regexp, text string) (ans []Match, err error) {
- m, err := re.FindStringMatch(text)
- if err != nil {
- return
- }
- rune_to_bytes := utils.RuneOffsetsToByteOffsets(text)
- get_byte_offset_map := func(groups []regexp2.Group) (ans map[int]int, err error) {
- ans = make(map[int]int, len(groups)*2)
- rune_offsets := make([]int, 0, len(groups)*2)
- for _, g := range groups {
- for _, c := range g.Captures {
- if _, found := ans[c.Index]; !found {
- rune_offsets = append(rune_offsets, c.Index)
- ans[c.Index] = -1
- }
- end := c.Index + c.Length
- if _, found := ans[end]; !found {
- rune_offsets = append(rune_offsets, end)
- ans[end] = -1
- }
- }
- }
- slices.Sort(rune_offsets)
- for _, pos := range rune_offsets {
- if ans[pos] = rune_to_bytes(pos); ans[pos] < 0 {
- return nil, fmt.Errorf("Matches are not monotonic cannot map rune offsets to byte offsets")
- }
- }
- return
- }
- for m != nil {
- groups := m.Groups()
- bom, err := get_byte_offset_map(groups)
- if err != nil {
- return nil, err
- }
- match := Match{Groups: make([]Group, len(groups))}
- for i, g := range m.Groups() {
- match.Groups[i].Name = g.Name
- match.Groups[i].IsNamed = g.Name != "" && g.Name != strconv.Itoa(i)
- for _, c := range g.Captures {
- cn := Capture{Text: c.String(), Text_as_runes: c.Runes()}
- cn.Rune_Offsets.End = c.Index + c.Length
- cn.Rune_Offsets.Start = c.Index
- cn.Byte_Offsets.Start, cn.Byte_Offsets.End = bom[c.Index], bom[cn.Rune_Offsets.End]
- match.Groups[i].Captures = append(match.Groups[i].Captures, cn)
- }
- }
- ans = append(ans, match)
- m, _ = re.FindNextMatch(m)
- }
- return
- }
- func mark(r *regexp2.Regexp, post_processors []PostProcessorFunc, group_processors []GroupProcessorFunc, text string, opts *Options) (ans []Mark) {
- sanitize_pat := regexp.MustCompile("[\r\n\x00]")
- all_matches, _ := find_all_matches(r, text)
- for i, m := range all_matches {
- full_capture := m.Groups[0].LastCapture()
- match_start, match_end := full_capture.Byte_Offsets.Start, full_capture.Byte_Offsets.End
- for match_end > match_start+1 && text[match_end-1] == 0 {
- match_end--
- }
- full_match := text[match_start:match_end]
- if len([]rune(full_match)) < opts.MinimumMatchLength {
- continue
- }
- for _, f := range post_processors {
- match_start, match_end = f(text, match_start, match_end)
- if match_start < 0 {
- break
- }
- }
- if match_start < 0 {
- continue
- }
- full_match = sanitize_pat.ReplaceAllLiteralString(text[match_start:match_end], "")
- gd := make(map[string]string, len(m.Groups))
- for idx, g := range m.Groups {
- if idx > 0 && g.IsNamed {
- c := g.LastCapture()
- if s, e := c.Byte_Offsets.Start, c.Byte_Offsets.End; s > -1 && e > -1 {
- s = max(s, match_start)
- e = min(e, match_end)
- gd[g.Name] = sanitize_pat.ReplaceAllLiteralString(text[s:e], "")
- }
- }
- }
- for _, f := range group_processors {
- f(gd)
- }
- gd2 := make(map[string]any, len(gd))
- for k, v := range gd {
- gd2[k] = v
- }
- if opts.Type == "regex" && len(m.Groups) > 1 && !m.HasNamedGroups() {
- cp := m.Groups[1].LastCapture()
- ms, me := cp.Byte_Offsets.Start, cp.Byte_Offsets.End
- match_start = max(match_start, ms)
- match_end = min(match_end, me)
- full_match = sanitize_pat.ReplaceAllLiteralString(text[match_start:match_end], "")
- }
- if full_match != "" {
- ans = append(ans, Mark{
- Index: i, Start: match_start, End: match_end, Text: full_match, Groupdict: gd2,
- })
- }
- }
- return
- }
- type ErrNoMatches struct{ Type, Pattern string }
- func is_word_char(ch rune, current_chars []rune) bool {
- return unicode.IsLetter(ch) || unicode.IsNumber(ch) || (unicode.IsMark(ch) && len(current_chars) > 0 && unicode.IsLetter(current_chars[len(current_chars)-1]))
- }
- func mark_words(text string, opts *Options) (ans []Mark) {
- left := text
- var current_run struct {
- chars []rune
- start, size int
- }
- chars := opts.WordCharacters
- if chars == "" {
- chars = RelevantKittyOpts().Select_by_word_characters
- }
- allowed_chars := make(map[rune]bool, len(chars))
- for _, ch := range chars {
- allowed_chars[ch] = true
- }
- pos := 0
- post_processors := []PostProcessorFunc{PostProcessorMap()["brackets"], PostProcessorMap()["quotes"]}
- commit_run := func() {
- if len(current_run.chars) >= opts.MinimumMatchLength {
- match_start, match_end := current_run.start, current_run.start+current_run.size
- for _, f := range post_processors {
- match_start, match_end = f(text, match_start, match_end)
- if match_start < 0 {
- break
- }
- }
- if match_start > -1 && match_end > match_start {
- full_match := text[match_start:match_end]
- if len([]rune(full_match)) >= opts.MinimumMatchLength {
- ans = append(ans, Mark{
- Index: len(ans), Start: match_start, End: match_end, Text: full_match,
- })
- }
- }
- }
- current_run.chars = nil
- current_run.start = 0
- current_run.size = 0
- }
- for {
- ch, size := utf8.DecodeRuneInString(left)
- if ch == utf8.RuneError {
- break
- }
- if allowed_chars[ch] || is_word_char(ch, current_run.chars) {
- if len(current_run.chars) == 0 {
- current_run.start = pos
- }
- current_run.chars = append(current_run.chars, ch)
- current_run.size += size
- } else {
- commit_run()
- }
- left = left[size:]
- pos += size
- }
- commit_run()
- return
- }
- func adjust_python_offsets(text string, marks []Mark) error {
- // python returns rune based offsets (unicode chars not utf-8 bytes)
- adjust := utils.RuneOffsetsToByteOffsets(text)
- for i := range marks {
- mark := &marks[i]
- if mark.End < mark.Start {
- return fmt.Errorf("The end of a mark must not be before its start")
- }
- s, e := adjust(mark.Start), adjust(mark.End)
- if s < 0 || e < 0 {
- return fmt.Errorf("Overlapping marks are not supported")
- }
- mark.Start, mark.End = s, e
- }
- return nil
- }
- func (self *ErrNoMatches) Error() string {
- none_of := "matches"
- switch self.Type {
- case "urls":
- none_of = "URLs"
- case "hyperlinks":
- none_of = "hyperlinks"
- }
- if self.Pattern != "" {
- return fmt.Sprintf("No %s found with pattern: %s", none_of, self.Pattern)
- }
- return fmt.Sprintf("No %s found", none_of)
- }
- func find_marks(text string, opts *Options, cli_args ...string) (sanitized_text string, ans []Mark, index_map map[int]*Mark, err error) {
- sanitized_text, hyperlinks := process_escape_codes(text)
- used_pattern := ""
- run_basic_matching := func() error {
- pattern, post_processors, group_processors, err := functions_for(opts)
- if err != nil {
- return err
- }
- r, err := regexp2.Compile(pattern, regexp2.RE2)
- if err != nil {
- return fmt.Errorf("Failed to compile the regex pattern: %#v with error: %w", pattern, err)
- }
- ans = mark(r, post_processors, group_processors, sanitized_text, opts)
- used_pattern = pattern
- return nil
- }
- if opts.CustomizeProcessing != "" {
- cmd := exec.Command(utils.KittyExe(), append([]string{"+runpy", "from kittens.hints.main import custom_marking; custom_marking()"}, cli_args...)...)
- cmd.Stdin = strings.NewReader(sanitized_text)
- stdout, stderr := bytes.Buffer{}, bytes.Buffer{}
- cmd.Stdout, cmd.Stderr = &stdout, &stderr
- err = cmd.Run()
- if err != nil {
- var e *exec.ExitError
- if errors.As(err, &e) && e.ExitCode() == 2 {
- err = run_basic_matching()
- if err != nil {
- return
- }
- goto process_answer
- } else {
- return "", nil, nil, fmt.Errorf("Failed to run custom processor %#v with error: %w\n%s", opts.CustomizeProcessing, err, stderr.String())
- }
- }
- ans = make([]Mark, 0, 32)
- err = json.Unmarshal(stdout.Bytes(), &ans)
- if err != nil {
- return "", nil, nil, fmt.Errorf("Failed to load output from custom processor %#v with error: %w", opts.CustomizeProcessing, err)
- }
- err = adjust_python_offsets(sanitized_text, ans)
- if err != nil {
- return "", nil, nil, fmt.Errorf("Custom processor %#v produced invalid mark output with error: %w", opts.CustomizeProcessing, err)
- }
- } else if opts.Type == "hyperlink" {
- ans = hyperlinks
- } else if opts.Type == "word" {
- ans = mark_words(sanitized_text, opts)
- } else {
- err = run_basic_matching()
- if err != nil {
- return
- }
- }
- process_answer:
- if len(ans) == 0 {
- return "", nil, nil, &ErrNoMatches{Type: opts.Type, Pattern: used_pattern}
- }
- largest_index := ans[len(ans)-1].Index
- offset := max(0, opts.HintsOffset)
- index_map = make(map[int]*Mark, len(ans))
- for i := range ans {
- m := &ans[i]
- if opts.Ascending {
- m.Index += offset
- } else {
- m.Index = largest_index - m.Index + offset
- }
- index_map[m.Index] = m
- }
- return
- }
|