187 lines
4.9 KiB
Go
187 lines
4.9 KiB
Go
package autoimport
|
|
|
|
import (
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"github.com/microsoft/typescript-go/internal/core"
|
|
)
|
|
|
|
// Named is a constraint for types that can provide their name.
|
|
type Named interface {
|
|
Name() string
|
|
}
|
|
|
|
// Index stores entries with an index mapping uppercase letters to entries whose name
|
|
// starts with that letter, and lowercase letters to entries whose name contains a
|
|
// word starting with that letter.
|
|
type Index[T Named] struct {
|
|
entries []T
|
|
index map[rune][]int
|
|
}
|
|
|
|
func (idx *Index[T]) Find(name string, caseSensitive bool) []T {
|
|
if len(idx.entries) == 0 || len(name) == 0 {
|
|
return nil
|
|
}
|
|
firstRune := core.FirstResult(utf8.DecodeRuneInString(name))
|
|
if firstRune == utf8.RuneError {
|
|
return nil
|
|
}
|
|
firstRuneUpper := unicode.ToUpper(firstRune)
|
|
candidates, ok := idx.index[firstRuneUpper]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
var results []T
|
|
for _, entryIndex := range candidates {
|
|
entry := idx.entries[entryIndex]
|
|
entryName := entry.Name()
|
|
if (caseSensitive && entryName == name) || (!caseSensitive && strings.EqualFold(entryName, name)) {
|
|
results = append(results, entry)
|
|
}
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
// SearchWordPrefix returns each entry whose name contains a word beginning with
|
|
// the first character of 'prefix', and whose name contains all characters
|
|
// of 'prefix' in order (case-insensitive). If 'filter' is provided, only entries
|
|
// for which filter(entry) returns true are included.
|
|
func (idx *Index[T]) SearchWordPrefix(prefix string) []T {
|
|
if len(idx.entries) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if len(prefix) == 0 {
|
|
return idx.entries
|
|
}
|
|
|
|
prefix = strings.ToLower(prefix)
|
|
firstRune, _ := utf8.DecodeRuneInString(prefix)
|
|
if firstRune == utf8.RuneError {
|
|
return nil
|
|
}
|
|
|
|
firstRuneUpper := unicode.ToUpper(firstRune)
|
|
firstRuneLower := unicode.ToLower(firstRune)
|
|
|
|
// Look up entries that have words starting with this letter
|
|
var wordStarts []int
|
|
nameStarts, _ := idx.index[firstRuneUpper]
|
|
if firstRuneUpper != firstRuneLower {
|
|
wordStarts, _ = idx.index[firstRuneLower]
|
|
}
|
|
count := len(nameStarts) + len(wordStarts)
|
|
if count == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Filter entries by checking if they contain all characters in order
|
|
results := make([]T, 0, count)
|
|
for _, starts := range [][]int{nameStarts, wordStarts} {
|
|
for _, i := range starts {
|
|
entry := idx.entries[i]
|
|
if containsCharsInOrder(entry.Name(), prefix) {
|
|
results = append(results, entry)
|
|
}
|
|
}
|
|
}
|
|
return results
|
|
}
|
|
|
|
// containsCharsInOrder checks if str contains all characters from pattern in order (case-insensitive).
|
|
func containsCharsInOrder(str, pattern string) bool {
|
|
str = strings.ToLower(str)
|
|
pattern = strings.ToLower(pattern)
|
|
|
|
patternIdx := 0
|
|
for _, ch := range str {
|
|
if patternIdx < len(pattern) {
|
|
patternRune, size := utf8.DecodeRuneInString(pattern[patternIdx:])
|
|
if ch == patternRune {
|
|
patternIdx += size
|
|
}
|
|
}
|
|
}
|
|
return patternIdx == len(pattern)
|
|
}
|
|
|
|
// insertAsWords adds a value to the index keyed by the first letter of each word in its name.
|
|
func (idx *Index[T]) insertAsWords(value T) {
|
|
if idx.index == nil {
|
|
idx.index = make(map[rune][]int)
|
|
}
|
|
|
|
name := value.Name()
|
|
if len(name) == 0 {
|
|
panic("Cannot index entry with empty name")
|
|
}
|
|
entryIndex := len(idx.entries)
|
|
idx.entries = append(idx.entries, value)
|
|
|
|
indices := wordIndices(name)
|
|
seenRunes := make(map[rune]bool)
|
|
|
|
for i, start := range indices {
|
|
substr := name[start:]
|
|
firstRune, _ := utf8.DecodeRuneInString(substr)
|
|
if firstRune == utf8.RuneError {
|
|
continue
|
|
}
|
|
if i == 0 {
|
|
// Name start keyed by uppercase
|
|
firstRune = unicode.ToUpper(firstRune)
|
|
idx.index[firstRune] = append(idx.index[firstRune], entryIndex)
|
|
seenRunes[firstRune] = true // (Still set seenRunes in case first character is non-alphabetic)
|
|
} else {
|
|
// Subsequent word starts keyed by lowercase
|
|
firstRune = unicode.ToLower(firstRune)
|
|
if !seenRunes[firstRune] {
|
|
idx.index[firstRune] = append(idx.index[firstRune], entryIndex)
|
|
seenRunes[firstRune] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clone creates a new Index containing only entries for which filter returns true.
|
|
func (idx *Index[T]) Clone(filter func(T) bool) *Index[T] {
|
|
if idx == nil {
|
|
return nil
|
|
}
|
|
|
|
newIdx := &Index[T]{
|
|
entries: make([]T, 0, len(idx.entries)),
|
|
index: make(map[rune][]int, len(idx.index)),
|
|
}
|
|
|
|
// Build mapping from old index to new index for filtered entries
|
|
oldToNew := make(map[int]int, len(idx.entries))
|
|
for oldIndex, entry := range idx.entries {
|
|
if filter(entry) {
|
|
newIndex := len(newIdx.entries)
|
|
newIdx.entries = append(newIdx.entries, entry)
|
|
oldToNew[oldIndex] = newIndex
|
|
}
|
|
}
|
|
|
|
// Rebuild the index with remapped indices
|
|
for r, oldIndices := range idx.index {
|
|
newIndices := make([]int, 0, len(oldIndices))
|
|
for _, oldIndex := range oldIndices {
|
|
if newIndex, ok := oldToNew[oldIndex]; ok {
|
|
newIndices = append(newIndices, newIndex)
|
|
}
|
|
}
|
|
if len(newIndices) > 0 {
|
|
newIdx.index[r] = newIndices
|
|
}
|
|
}
|
|
|
|
return newIdx
|
|
}
|