github.com/status-im/status-go@v1.1.0/protocol/messenger_mention.go (about)

     1  package protocol
     2  
     3  // this is a reimplementation of the mention feature in status-react
     4  // reference implementation: https://github.com/status-im/status-react/blob/972347963498fc4a2bb8f85541e79ed0541698da/src/status_im/chat/models/mentions.cljs#L1
     5  import (
     6  	"encoding/json"
     7  	"fmt"
     8  	"regexp"
     9  	"strings"
    10  	"sync"
    11  	"unicode"
    12  	"unicode/utf8"
    13  
    14  	"go.uber.org/zap"
    15  
    16  	"github.com/status-im/status-go/logutils"
    17  
    18  	"github.com/status-im/status-go/api/multiformat"
    19  	"github.com/status-im/status-go/protocol/common"
    20  )
    21  
    22  const (
    23  	endingChars = `[\s\.,;:]`
    24  
    25  	charAtSign     = "@"
    26  	charQuote      = ">"
    27  	charNewline    = "\n"
    28  	charAsterisk   = "*"
    29  	charUnderscore = "_"
    30  	charTilde      = "~"
    31  	charCodeBlock  = "`"
    32  
    33  	intUnknown = -1
    34  
    35  	suggestionsLimit = 10
    36  )
    37  
    38  var (
    39  	specialCharsRegex = regexp.MustCompile("[@~\\\\*_\n>`]{1}")
    40  	endingCharsRegex  = regexp.MustCompile(endingChars)
    41  	wordRegex         = regexp.MustCompile("^[\\w\\d\\-]*" + endingChars + "|[\\S]+")
    42  )
    43  
    44  type specialCharLocation struct {
    45  	Index int
    46  	Value string
    47  }
    48  
    49  type atSignIndex struct {
    50  	Pending []int
    51  	Checked []int
    52  }
    53  
    54  type styleTag struct {
    55  	Len int
    56  	Idx int
    57  }
    58  
    59  type textMeta struct {
    60  	atSign         *atSignIndex
    61  	styleTagMap    map[string]*styleTag
    62  	quoteIndex     *int
    63  	newlineIndexes []int
    64  }
    65  
    66  type searchablePhrase struct {
    67  	originalName string
    68  	phrase       string
    69  }
    70  
    71  type MentionableUser struct {
    72  	*Contact
    73  
    74  	searchablePhrases []searchablePhrase
    75  
    76  	Key          string // a unique identifier of a mentionable user
    77  	Match        string
    78  	SearchedText string
    79  }
    80  
    81  func (c *MentionableUser) MarshalJSON() ([]byte, error) {
    82  	compressedKey, err := multiformat.SerializeLegacyKey(c.ID)
    83  	if err != nil {
    84  		return nil, err
    85  	}
    86  
    87  	type MentionableUserJSON struct {
    88  		PrimaryName   string `json:"primaryName"`
    89  		SecondaryName string `json:"secondaryName"`
    90  		ENSVerified   bool   `json:"ensVerified"`
    91  		Added         bool   `json:"added"`
    92  		DisplayName   string `json:"displayName"`
    93  		Key           string `json:"key"`
    94  		Match         string `json:"match"`
    95  		SearchedText  string `json:"searchedText"`
    96  		ID            string `json:"id"`
    97  		CompressedKey string `json:"compressedKey,omitempty"`
    98  	}
    99  
   100  	contactJSON := MentionableUserJSON{
   101  		PrimaryName:   c.PrimaryName(),
   102  		SecondaryName: c.SecondaryName(),
   103  		ENSVerified:   c.ENSVerified,
   104  		Added:         c.added(),
   105  		DisplayName:   c.GetDisplayName(),
   106  		Key:           c.Key,
   107  		Match:         c.Match,
   108  		SearchedText:  c.SearchedText,
   109  		ID:            c.ID,
   110  		CompressedKey: compressedKey,
   111  	}
   112  
   113  	return json.Marshal(contactJSON)
   114  }
   115  
   116  func (c *MentionableUser) GetDisplayName() string {
   117  	if c.ENSVerified && c.EnsName != "" {
   118  		return c.EnsName
   119  	}
   120  	if c.DisplayName != "" {
   121  		return c.DisplayName
   122  	}
   123  	if c.PrimaryName() != "" {
   124  		return c.PrimaryName()
   125  	}
   126  	return c.Alias
   127  }
   128  
   129  type SegmentType int
   130  
   131  const (
   132  	Text SegmentType = iota
   133  	Mention
   134  )
   135  
   136  type InputSegment struct {
   137  	Type  SegmentType `json:"type"`
   138  	Value string      `json:"value"`
   139  }
   140  
   141  type MentionState struct {
   142  	AtSignIdx    int
   143  	AtIdxs       []*AtIndexEntry
   144  	MentionEnd   int
   145  	PreviousText string
   146  	NewText      string
   147  	Start        int
   148  	End          int
   149  	operation    textOperation
   150  }
   151  
   152  func (ms *MentionState) String() string {
   153  	atIdxsStr := ""
   154  	for i, entry := range ms.AtIdxs {
   155  		if i > 0 {
   156  			atIdxsStr += ", "
   157  		}
   158  		atIdxsStr += fmt.Sprintf("%+v", entry)
   159  	}
   160  	return fmt.Sprintf("MentionState{AtSignIdx: %d, AtIdxs: [%s], MentionEnd: %d, PreviousText: %q, NewText: %s, Start: %d, End: %d}",
   161  		ms.AtSignIdx, atIdxsStr, ms.MentionEnd, ms.PreviousText, ms.NewText, ms.Start, ms.End)
   162  }
   163  
   164  type ChatMentionContext struct {
   165  	ChatID             string
   166  	InputSegments      []InputSegment
   167  	MentionSuggestions map[string]*MentionableUser
   168  	MentionState       *MentionState
   169  	PreviousText       string // user input text before the last change
   170  	NewText            string
   171  
   172  	CallID       uint64
   173  	mu           *sync.Mutex
   174  	LatestCallID uint64
   175  }
   176  
   177  func NewChatMentionContext(chatID string) *ChatMentionContext {
   178  	return &ChatMentionContext{
   179  		ChatID:             chatID,
   180  		MentionSuggestions: make(map[string]*MentionableUser),
   181  		MentionState:       new(MentionState),
   182  		mu:                 new(sync.Mutex),
   183  	}
   184  }
   185  
   186  type mentionableUserGetter interface {
   187  	getMentionableUsers(chatID string) (map[string]*MentionableUser, error)
   188  	getMentionableUser(chatID string, pk string) (*MentionableUser, error)
   189  }
   190  
   191  type MentionManager struct {
   192  	mentionContexts map[string]*ChatMentionContext
   193  	*Messenger
   194  	mentionableUserGetter
   195  	logger *zap.Logger
   196  }
   197  
   198  func NewMentionManager(m *Messenger) *MentionManager {
   199  	mm := &MentionManager{
   200  		mentionContexts: make(map[string]*ChatMentionContext),
   201  		Messenger:       m,
   202  		logger:          logutils.ZapLogger().Named("MentionManager"),
   203  	}
   204  	mm.mentionableUserGetter = mm
   205  	return mm
   206  }
   207  
   208  func (m *MentionManager) getChatMentionContext(chatID string) *ChatMentionContext {
   209  	ctx, ok := m.mentionContexts[chatID]
   210  	if !ok {
   211  		ctx = NewChatMentionContext(chatID)
   212  		m.mentionContexts[chatID] = ctx
   213  	}
   214  	return ctx
   215  }
   216  
   217  func (m *MentionManager) getMentionableUser(chatID string, pk string) (*MentionableUser, error) {
   218  	mentionableUsers, err := m.mentionableUserGetter.getMentionableUsers(chatID)
   219  	if err != nil {
   220  		return nil, err
   221  	}
   222  	user, ok := mentionableUsers[pk]
   223  	if !ok {
   224  		return nil, fmt.Errorf("user not found when getting mentionable user, pk: %s", pk)
   225  	}
   226  	return user, nil
   227  }
   228  
   229  func (m *MentionManager) getMentionableUsers(chatID string) (map[string]*MentionableUser, error) {
   230  	mentionableUsers := make(map[string]*MentionableUser)
   231  	chat, _ := m.allChats.Load(chatID)
   232  	if chat == nil {
   233  		return nil, fmt.Errorf("chat not found when getting mentionable users, chatID: %s", chatID)
   234  	}
   235  
   236  	var publicKeys []string
   237  	switch {
   238  	case chat.PrivateGroupChat():
   239  		for _, mb := range chat.Members {
   240  			publicKeys = append(publicKeys, mb.ID)
   241  		}
   242  	case chat.OneToOne():
   243  		publicKeys = append(publicKeys, chatID)
   244  	case chat.CommunityChat():
   245  		community, err := m.communitiesManager.GetByIDString(chat.CommunityID)
   246  		if err != nil {
   247  			return nil, err
   248  		}
   249  		for _, pk := range community.GetMemberPubkeys() {
   250  			publicKeys = append(publicKeys, common.PubkeyToHex(pk))
   251  		}
   252  	case chat.Public():
   253  		m.allContacts.Range(func(pk string, _ *Contact) bool {
   254  			publicKeys = append(publicKeys, pk)
   255  			return true
   256  		})
   257  	}
   258  
   259  	var me = m.myHexIdentity()
   260  	for _, pk := range publicKeys {
   261  		if pk == me {
   262  			continue
   263  		}
   264  		if err := m.addMentionableUser(mentionableUsers, pk); err != nil {
   265  			return nil, err
   266  		}
   267  	}
   268  	return mentionableUsers, nil
   269  }
   270  
   271  func (m *MentionManager) addMentionableUser(mentionableUsers map[string]*MentionableUser, publicKey string) error {
   272  	contact, ok := m.allContacts.Load(publicKey)
   273  	if !ok {
   274  		c, err := buildContactFromPkString(publicKey)
   275  		if err != nil {
   276  			return err
   277  		}
   278  		contact = c
   279  	}
   280  	user := &MentionableUser{
   281  		Contact: contact,
   282  	}
   283  	user = addSearchablePhrases(user)
   284  	if user != nil {
   285  		mentionableUsers[publicKey] = user
   286  	}
   287  	return nil
   288  }
   289  
   290  func (m *MentionManager) ReplaceWithPublicKey(chatID, text string) (string, error) {
   291  	chat, _ := m.allChats.Load(chatID)
   292  	if chat == nil {
   293  		return "", fmt.Errorf("chat not found when check mentions, chatID: %s", chatID)
   294  	}
   295  	mentionableUsers, err := m.mentionableUserGetter.getMentionableUsers(chatID)
   296  	if err != nil {
   297  		return "", err
   298  	}
   299  	newText := ReplaceMentions(text, mentionableUsers)
   300  	m.ClearMentions(chatID)
   301  	return newText, nil
   302  }
   303  
   304  func withCallID(ctx *ChatMentionContext, callID uint64) *ChatMentionContext {
   305  	result := *ctx
   306  	result.CallID = callID
   307  	return &result
   308  }
   309  
   310  func (m *MentionManager) OnChangeText(chatID, fullText string, callID uint64) (*ChatMentionContext, error) {
   311  	ctx := m.getChatMentionContext(chatID)
   312  	if callID > 0 {
   313  		ctx.mu.Lock()
   314  		if callID <= ctx.LatestCallID {
   315  			ctx.mu.Unlock()
   316  			return withCallID(ctx, callID), fmt.Errorf("callID is less than or equal to latestCallID, callID: %d, maxCallID: %d", callID, ctx.LatestCallID)
   317  		}
   318  		ctx.LatestCallID = callID
   319  		ctx.mu.Unlock()
   320  	}
   321  
   322  	diff := diffText(ctx.PreviousText, fullText)
   323  	if diff == nil {
   324  		return withCallID(ctx, callID), nil
   325  	}
   326  	ctx.PreviousText = fullText
   327  	if ctx.MentionState == nil {
   328  		ctx.MentionState = &MentionState{}
   329  	}
   330  	ctx.MentionState.PreviousText = diff.previousText
   331  	ctx.MentionState.NewText = diff.newText
   332  	ctx.MentionState.Start = diff.start
   333  	ctx.MentionState.End = diff.end
   334  	ctx.MentionState.operation = diff.operation
   335  
   336  	atIndexes, err := calculateAtIndexEntries(ctx.MentionState)
   337  	if err != nil {
   338  		return withCallID(ctx, callID), err
   339  	}
   340  	ctx.MentionState.AtIdxs = atIndexes
   341  	m.logger.Debug("OnChangeText", zap.String("chatID", chatID), zap.Any("state", ctx.MentionState))
   342  	ctx, err = m.calculateSuggestions(chatID, fullText)
   343  	return withCallID(ctx, callID), err
   344  }
   345  
   346  func (m *MentionManager) calculateSuggestions(chatID, fullText string) (*ChatMentionContext, error) {
   347  	ctx := m.getChatMentionContext(chatID)
   348  
   349  	mentionableUsers, err := m.mentionableUserGetter.getMentionableUsers(chatID)
   350  	if err != nil {
   351  		return nil, err
   352  	}
   353  	m.logger.Debug("calculateSuggestions", zap.String("chatID", chatID), zap.String("fullText", fullText), zap.Int("num of mentionable user", len(mentionableUsers)))
   354  
   355  	m.calculateSuggestionsWithMentionableUsers(chatID, fullText, mentionableUsers)
   356  
   357  	return ctx, nil
   358  }
   359  
   360  func (m *MentionManager) calculateSuggestionsWithMentionableUsers(chatID string, fullText string, mentionableUsers map[string]*MentionableUser) {
   361  	ctx := m.getChatMentionContext(chatID)
   362  	state := ctx.MentionState
   363  
   364  	if len(state.AtIdxs) == 0 {
   365  		state.AtIdxs = nil
   366  		ctx.MentionSuggestions = nil
   367  		ctx.InputSegments = []InputSegment{{
   368  			Type:  Text,
   369  			Value: fullText,
   370  		}}
   371  		return
   372  	}
   373  
   374  	newAtIndexEntries := checkIdxForMentions(fullText, state.AtIdxs, mentionableUsers)
   375  	calculatedInput, success := calculateInput(fullText, newAtIndexEntries)
   376  	if !success {
   377  		m.logger.Warn("calculateSuggestionsWithMentionableUsers: calculateInput failed", zap.String("chatID", chatID), zap.String("fullText", fullText), zap.Any("state", state))
   378  	}
   379  
   380  	var end int
   381  	switch state.operation {
   382  	case textOperationAdd:
   383  		end = state.Start + len([]rune(state.NewText))
   384  	case textOperationDelete:
   385  		end = state.Start
   386  	case textOperationReplace:
   387  		end = state.Start + len([]rune(state.NewText))
   388  	default:
   389  		m.logger.Error("calculateSuggestionsWithMentionableUsers: unknown textOperation", zap.String("chatID", chatID), zap.String("fullText", fullText), zap.Any("state", state))
   390  	}
   391  
   392  	atSignIdx := lastIndexOfAtSign(fullText, end)
   393  	var suggestions map[string]*MentionableUser
   394  	if atSignIdx != -1 {
   395  		searchedText := strings.ToLower(subs(fullText, atSignIdx+1, end))
   396  		m.logger.Debug("calculateSuggestionsWithMentionableUsers", zap.Int("atSignIdx", atSignIdx), zap.String("searchedText", searchedText), zap.String("fullText", fullText), zap.Any("state", state), zap.Int("end", end))
   397  		if end-atSignIdx <= 100 {
   398  			suggestions = getUserSuggestions(mentionableUsers, searchedText, suggestionsLimit)
   399  		}
   400  	}
   401  
   402  	state.AtSignIdx = atSignIdx
   403  	state.AtIdxs = newAtIndexEntries
   404  	state.MentionEnd = end
   405  	ctx.InputSegments = calculatedInput
   406  	ctx.MentionSuggestions = suggestions
   407  }
   408  
   409  func (m *MentionManager) SelectMention(chatID, text, primaryName, publicKey string) (*ChatMentionContext, error) {
   410  	ctx := m.getChatMentionContext(chatID)
   411  	state := ctx.MentionState
   412  
   413  	atSignIdx := state.AtSignIdx
   414  	mentionEnd := state.MentionEnd
   415  
   416  	var nextChar rune
   417  	tr := []rune(text)
   418  	if mentionEnd < len(tr) {
   419  		nextChar = tr[mentionEnd]
   420  	}
   421  
   422  	space := ""
   423  	if string(nextChar) == "" || (!unicode.IsSpace(nextChar)) {
   424  		space = " "
   425  	}
   426  
   427  	ctx.NewText = string(tr[:atSignIdx+1]) + primaryName + space + string(tr[mentionEnd:])
   428  
   429  	_, err := m.OnChangeText(chatID, ctx.NewText, 0)
   430  	if err != nil {
   431  		return nil, err
   432  	}
   433  	m.clearSuggestions(chatID)
   434  	return ctx, nil
   435  }
   436  
   437  func (m *MentionManager) clearSuggestions(chatID string) {
   438  	m.getChatMentionContext(chatID).MentionSuggestions = nil
   439  }
   440  
   441  func (m *MentionManager) ClearMentions(chatID string) {
   442  	ctx := m.getChatMentionContext(chatID)
   443  	ctx.MentionState = nil
   444  	ctx.InputSegments = nil
   445  	ctx.NewText = ""
   446  	ctx.PreviousText = ""
   447  	m.clearSuggestions(chatID)
   448  }
   449  
   450  func (m *MentionManager) ToInputField(chatID, text string) (*ChatMentionContext, error) {
   451  	mentionableUsers, err := m.mentionableUserGetter.getMentionableUsers(chatID)
   452  	if err != nil {
   453  		return nil, err
   454  	}
   455  	textWithMentions := toInputField(text)
   456  	newText := ""
   457  	for i, segment := range textWithMentions {
   458  		if segment.Type == Mention {
   459  			mentionableUser := mentionableUsers[segment.Value]
   460  			mention := mentionableUser.GetDisplayName()
   461  			if !strings.HasPrefix(mention, charAtSign) {
   462  				segment.Value = charAtSign + mention
   463  			}
   464  			textWithMentions[i] = segment
   465  		}
   466  		newText += segment.Value
   467  	}
   468  	ctx := m.getChatMentionContext(chatID)
   469  	ctx.InputSegments = textWithMentions
   470  	ctx.MentionState = toInfo(textWithMentions)
   471  	ctx.NewText = newText
   472  	ctx.PreviousText = newText
   473  	return ctx, nil
   474  }
   475  
   476  func rePos(s string) []specialCharLocation {
   477  	var res []specialCharLocation
   478  	lastMatch := specialCharsRegex.FindStringIndex(s)
   479  	for lastMatch != nil {
   480  		start, end := lastMatch[0], lastMatch[1]
   481  		c := s[start:end]
   482  		res = append(res, specialCharLocation{utf8.RuneCountInString(s[:start]), c})
   483  		lastMatch = specialCharsRegex.FindStringIndex(s[end:])
   484  		if lastMatch != nil {
   485  			lastMatch[0] += end
   486  			lastMatch[1] += end
   487  		}
   488  	}
   489  	return res
   490  }
   491  
   492  func codeTagLen(idxs []specialCharLocation, idx int) int {
   493  	pos, c := idxs[idx].Index, idxs[idx].Value
   494  
   495  	next := func(n int) (int, string) {
   496  		if n < len(idxs) {
   497  			return idxs[n].Index, idxs[n].Value
   498  		}
   499  		return 0, ""
   500  	}
   501  
   502  	pos2, c2 := next(idx + 1)
   503  	pos3, c3 := next(idx + 2)
   504  
   505  	if c == c2 && pos == pos2-1 && c2 == c3 && pos == pos3-2 {
   506  		return 3
   507  	}
   508  
   509  	if c == c2 && pos == pos2-1 {
   510  		return 2
   511  	}
   512  
   513  	return 1
   514  }
   515  
   516  func clearPendingAtSigns(data *textMeta, from int) {
   517  	newIdxs := make([]int, 0)
   518  	for _, idx := range data.atSign.Pending {
   519  		if idx < from {
   520  			newIdxs = append(newIdxs, idx)
   521  		}
   522  	}
   523  	data.atSign.Pending = []int{}
   524  	data.atSign.Checked = append(data.atSign.Checked, newIdxs...)
   525  }
   526  
   527  func checkStyleTag(text string, idxs []specialCharLocation, idx int) (length int, canBeStart bool, canBeEnd bool) {
   528  	pos, c := idxs[idx].Index, idxs[idx].Value
   529  	tr := []rune(text)
   530  	next := func(n int) (int, string) {
   531  		if n < len(idxs) {
   532  			return idxs[n].Index, idxs[n].Value
   533  		}
   534  		return len(tr), ""
   535  	}
   536  
   537  	pos2, c2 := next(idx + 1)
   538  	pos3, c3 := next(idx + 2)
   539  
   540  	switch {
   541  	case c == c2 && c2 == c3 && pos == pos2-1 && pos == pos3-2:
   542  		length = 3
   543  	case c == c2 && pos == pos2-1:
   544  		length = 2
   545  	default:
   546  		length = 1
   547  	}
   548  
   549  	var prevC, nextC *rune
   550  	if decPos := pos - 1; decPos >= 0 {
   551  		prevC = &tr[decPos]
   552  	}
   553  
   554  	nextIdx := idxs[idx+length-1].Index + 1
   555  	if nextIdx < len(tr) {
   556  		nextC = &tr[nextIdx]
   557  	}
   558  
   559  	if length == 1 {
   560  		canBeEnd = prevC != nil && !unicode.IsSpace(*prevC) && (nextC == nil || unicode.IsSpace(*nextC))
   561  	} else {
   562  		canBeEnd = prevC != nil && !unicode.IsSpace(*prevC)
   563  	}
   564  
   565  	canBeStart = nextC != nil && !unicode.IsSpace(*nextC)
   566  	return length, canBeStart, canBeEnd
   567  }
   568  
   569  func applyStyleTag(data *textMeta, idx int, pos int, c string, len int, start bool, end bool) int {
   570  	tag := data.styleTagMap[c]
   571  	tripleTilde := c == charTilde && len == 3
   572  
   573  	if tag != nil && end {
   574  		oldLen := (*tag).Len
   575  		var tagLen int
   576  		if tripleTilde && oldLen == 3 {
   577  			tagLen = 2
   578  		} else if oldLen >= len {
   579  			tagLen = len
   580  		} else {
   581  			tagLen = oldLen
   582  		}
   583  		oldIdx := (*tag).Idx
   584  		delete(data.styleTagMap, c)
   585  		clearPendingAtSigns(data, oldIdx)
   586  		return idx + tagLen
   587  	} else if start {
   588  		data.styleTagMap[c] = &styleTag{
   589  			Len: len,
   590  			Idx: pos,
   591  		}
   592  		clearPendingAtSigns(data, pos)
   593  	}
   594  	return idx + len
   595  }
   596  
   597  func newTextMeta() *textMeta {
   598  	return &textMeta{
   599  		atSign:      new(atSignIndex),
   600  		styleTagMap: make(map[string]*styleTag),
   601  	}
   602  }
   603  
   604  func newDataWithAtSignAndQuoteIndex(atSign *atSignIndex, quoteIndex *int) *textMeta {
   605  	data := newTextMeta()
   606  	data.atSign = atSign
   607  	data.quoteIndex = quoteIndex
   608  	return data
   609  }
   610  
   611  func getAtSigns(text string) []int {
   612  	idxs := rePos(text)
   613  	data := newTextMeta()
   614  	nextIdx := 0
   615  	tr := []rune(text)
   616  	for i := range idxs {
   617  		if i != nextIdx {
   618  			continue
   619  		}
   620  		nextIdx = i + 1
   621  		quoteIndex := data.quoteIndex
   622  		c := idxs[i].Value
   623  		pos := idxs[i].Index
   624  		switch {
   625  		case c == charNewline:
   626  			prevNewline := intUnknown
   627  			if len(data.newlineIndexes) > 0 {
   628  				prevNewline = data.newlineIndexes[0]
   629  			}
   630  
   631  			data.newlineIndexes = append(data.newlineIndexes, pos)
   632  
   633  			if quoteIndex != nil && prevNewline != intUnknown && strings.TrimSpace(string(tr[prevNewline:pos-1])) == "" {
   634  				data.quoteIndex = nil
   635  			}
   636  		case quoteIndex != nil:
   637  			continue
   638  		case c == charQuote:
   639  			prevNewlines := make([]int, 0, 2)
   640  			if len(data.newlineIndexes) > 0 {
   641  				prevNewlines = data.newlineIndexes
   642  			}
   643  
   644  			if pos == 0 ||
   645  				(len(prevNewlines) == 1 && strings.TrimSpace(string(tr[:pos-1])) == "") ||
   646  				(len(prevNewlines) == 2 && strings.TrimSpace(string(tr[prevNewlines[0]:pos-1])) == "") {
   647  				data = newDataWithAtSignAndQuoteIndex(data.atSign, &pos)
   648  			}
   649  		case c == charAtSign:
   650  			data.atSign.Pending = append(data.atSign.Pending, pos)
   651  		case c == charCodeBlock:
   652  			length := codeTagLen(idxs, i)
   653  			nextIdx = applyStyleTag(data, i, pos, c, length, true, true)
   654  		case c == charAsterisk || c == charUnderscore || c == charTilde:
   655  			length, canBeStart, canBeEnd := checkStyleTag(text, idxs, i)
   656  			nextIdx = applyStyleTag(data, i, pos, c, length, canBeStart, canBeEnd)
   657  		}
   658  	}
   659  
   660  	return append(data.atSign.Checked, data.atSign.Pending...)
   661  }
   662  
   663  func getUserSuggestions(users map[string]*MentionableUser, searchedText string, limit int) map[string]*MentionableUser {
   664  	result := make(map[string]*MentionableUser)
   665  	for pk, user := range users {
   666  		match := findMatch(user, searchedText)
   667  		if match != "" {
   668  			result[pk] = &MentionableUser{
   669  				searchablePhrases: user.searchablePhrases,
   670  				Contact:           user.Contact,
   671  				Key:               pk,
   672  				Match:             match,
   673  				SearchedText:      searchedText,
   674  			}
   675  		}
   676  		if limit != -1 && len(result) >= limit {
   677  			break
   678  		}
   679  	}
   680  	return result
   681  }
   682  
   683  // findMatch searches for a matching phrase in MentionableUser's searchable phrases or names.
   684  func findMatch(user *MentionableUser, searchedText string) string {
   685  	if len(user.searchablePhrases) > 0 {
   686  		return findMatchInPhrases(user, searchedText)
   687  	}
   688  	return findMatchInNames(user, searchedText)
   689  }
   690  
   691  // findMatchInPhrases searches for a matching phrase in MentionableUser's searchable phrases.
   692  func findMatchInPhrases(user *MentionableUser, searchedText string) string {
   693  	var match string
   694  
   695  	for _, p := range user.searchablePhrases {
   696  		if searchedText == "" || strings.HasPrefix(strings.ToLower(p.phrase), searchedText) {
   697  			match = p.originalName
   698  			break
   699  		}
   700  	}
   701  
   702  	return match
   703  }
   704  
   705  // findMatchInNames searches for a matching phrase in MentionableUser's primary and secondary names.
   706  func findMatchInNames(user *MentionableUser, searchedText string) string {
   707  	var match string
   708  	for _, name := range user.names() {
   709  		if hasMatchingPrefix(name, searchedText) {
   710  			match = name
   711  		}
   712  	}
   713  	return match
   714  }
   715  
   716  // hasMatchingPrefix checks if the given text has a matching prefix with the searched text.
   717  func hasMatchingPrefix(text, searchedText string) bool {
   718  	return text != "" && (searchedText == "" || strings.HasPrefix(strings.ToLower(text), searchedText))
   719  }
   720  
   721  func isMentioned(user *MentionableUser, text string) bool {
   722  	regexStr := ""
   723  	for i, name := range user.names() {
   724  		if name == "" {
   725  			continue
   726  		}
   727  		name = strings.ToLower(name)
   728  		if i != 0 {
   729  			regexStr += "|"
   730  		}
   731  		regexStr += "^" + name + endingChars + "|" + "^" + name + "$"
   732  	}
   733  	regex := regexp.MustCompile(regexStr)
   734  	lCaseText := strings.ToLower(text)
   735  	return regex.MatchString(lCaseText)
   736  }
   737  
   738  func MatchMention(text string, users map[string]*MentionableUser, mentionKeyIdx int) *MentionableUser {
   739  	return matchMention(text, users, mentionKeyIdx, mentionKeyIdx+1, nil)
   740  }
   741  
   742  func matchMention(text string, users map[string]*MentionableUser, mentionKeyIdx int, nextWordIdx int, words []string) *MentionableUser {
   743  	tr := []rune(text)
   744  	if nextWordIdx >= len(tr) {
   745  		return nil
   746  	}
   747  	if word := wordRegex.FindString(string(tr[nextWordIdx:])); word != "" {
   748  		newWords := append(words, word)
   749  
   750  		t := strings.TrimSpace(strings.ToLower(strings.Join(newWords, "")))
   751  		tt := []rune(t)
   752  		searchedText := t
   753  		if lastChar := len(tt) - 1; lastChar >= 0 && endingCharsRegex.MatchString(string(tt[lastChar:])) {
   754  			searchedText = string(tt[:lastChar])
   755  		}
   756  
   757  		userSuggestions := getUserSuggestions(users, searchedText, suggestionsLimit)
   758  		userSuggestionsCnt := len(userSuggestions)
   759  		switch {
   760  		case userSuggestionsCnt == 0:
   761  			return nil
   762  		case userSuggestionsCnt == 1:
   763  			user := getFirstUser(userSuggestions)
   764  			// maybe len(users) == 1 and user input `@` so we need to recheck if the user is really mentioned
   765  			if isMentioned(user, string(tr[mentionKeyIdx+1:])) {
   766  				return user
   767  			}
   768  		case userSuggestionsCnt > 1:
   769  			wordLen := len([]rune(word))
   770  			textLen := len(tr)
   771  			nextWordStart := nextWordIdx + wordLen
   772  			if textLen > nextWordStart {
   773  				user := matchMention(text, users, mentionKeyIdx, nextWordStart, newWords)
   774  				if user != nil {
   775  					return user
   776  				}
   777  			}
   778  			return filterWithFullMatch(userSuggestions, searchedText)
   779  		}
   780  	}
   781  	return nil
   782  }
   783  
   784  func filterWithFullMatch(userSuggestions map[string]*MentionableUser, text string) *MentionableUser {
   785  	if text == "" {
   786  		return nil
   787  	}
   788  	result := make(map[string]*MentionableUser)
   789  	for pk, user := range userSuggestions {
   790  		for _, name := range user.names() {
   791  			if strings.ToLower(name) == text {
   792  				result[pk] = user
   793  			}
   794  		}
   795  	}
   796  	return getFirstUser(result)
   797  }
   798  
   799  func getFirstUser(userSuggestions map[string]*MentionableUser) *MentionableUser {
   800  	for _, user := range userSuggestions {
   801  		return user
   802  	}
   803  	return nil
   804  }
   805  
   806  func ReplaceMentions(text string, users map[string]*MentionableUser) string {
   807  	idxs := getAtSigns(text)
   808  	return replaceMentions(text, users, idxs, 0)
   809  }
   810  
   811  func replaceMentions(text string, users map[string]*MentionableUser, idxs []int, diff int) string {
   812  	if strings.TrimSpace(text) == "" || len(idxs) == 0 {
   813  		return text
   814  	}
   815  
   816  	mentionKeyIdx := idxs[0] - diff
   817  
   818  	if len(users) == 0 {
   819  		return text
   820  	}
   821  
   822  	matchUser := MatchMention(text, users, mentionKeyIdx)
   823  	if matchUser == nil {
   824  		return replaceMentions(text, users, idxs[1:], diff)
   825  	}
   826  
   827  	tr := []rune(text)
   828  	newText := string(tr[:mentionKeyIdx+1]) + matchUser.ID + string(tr[mentionKeyIdx+1+len([]rune(matchUser.Match)):])
   829  	newDiff := diff + len(tr) - len([]rune(newText))
   830  
   831  	return replaceMentions(newText, users, idxs[1:], newDiff)
   832  }
   833  
   834  func addSearchablePhrases(user *MentionableUser) *MentionableUser {
   835  	if !user.Blocked {
   836  		searchablePhrases := user.names()
   837  		for _, s := range searchablePhrases {
   838  			if s != "" {
   839  				newWords := []string{s}
   840  				newWords = append(newWords, strings.Split(s, " ")[1:]...)
   841  				var phrases []searchablePhrase
   842  				for _, w := range newWords {
   843  					phrases = append(phrases, searchablePhrase{s, w})
   844  				}
   845  				user.searchablePhrases = append(user.searchablePhrases, phrases...)
   846  			}
   847  		}
   848  		return user
   849  	}
   850  	return nil
   851  }
   852  
   853  type AtIndexEntry struct {
   854  	From    int
   855  	To      int
   856  	Checked bool
   857  
   858  	Mentioned bool
   859  	NextAtIdx int
   860  }
   861  
   862  func (e *AtIndexEntry) String() string {
   863  	return fmt.Sprintf("{From: %d, To: %d, Checked: %t, Mentioned: %t, NextAtIdx: %d}", e.From, e.To, e.Checked, e.Mentioned, e.NextAtIdx)
   864  }
   865  
   866  func calculateAtIndexEntries(state *MentionState) ([]*AtIndexEntry, error) {
   867  	var keptAtIndexEntries []*AtIndexEntry
   868  	var oldRunes []rune
   869  	var newRunes []rune
   870  	var previousRunes = []rune(state.PreviousText)
   871  	switch state.operation {
   872  	case textOperationAdd:
   873  		newRunes = []rune(state.NewText)
   874  	case textOperationDelete:
   875  		oldRunes = previousRunes[state.Start : state.End+1]
   876  	case textOperationReplace:
   877  		oldRunes = previousRunes[state.Start : state.End+1]
   878  		newRunes = []rune(state.NewText)
   879  	default:
   880  		return nil, fmt.Errorf("unknown text operation: %d", state.operation)
   881  	}
   882  
   883  	oldLen := len(oldRunes)
   884  	newLen := len(newRunes)
   885  	diff := newLen - oldLen
   886  	oldAtSignIndexes := getAtSignIdxs(string(oldRunes), state.Start)
   887  	newAtSignIndexes := getAtSignIdxs(state.NewText, state.Start)
   888  	for _, entry := range state.AtIdxs {
   889  		deleted := false
   890  		for _, idx := range oldAtSignIndexes {
   891  			if idx == entry.From {
   892  				deleted = true
   893  			}
   894  		}
   895  		if !deleted {
   896  			if entry.From >= state.Start { // update range with diff
   897  				entry.From += diff
   898  				entry.To += diff
   899  			}
   900  			if entry.From < state.Start && entry.To+1 >= state.Start { // impacted after user edit so need to be rechecked
   901  				entry.Checked = false
   902  			}
   903  			keptAtIndexEntries = append(keptAtIndexEntries, entry)
   904  		}
   905  	}
   906  	return addNewAtSignIndexes(keptAtIndexEntries, newAtSignIndexes), nil
   907  }
   908  
   909  func addNewAtSignIndexes(keptAtIdxs []*AtIndexEntry, newAtSignIndexes []int) []*AtIndexEntry {
   910  	var newAtIndexEntries []*AtIndexEntry
   911  	var added bool
   912  	var lastNewIdx int
   913  	newAtSignIndexesCount := len(newAtSignIndexes)
   914  	if newAtSignIndexesCount > 0 {
   915  		lastNewIdx = newAtSignIndexes[newAtSignIndexesCount-1]
   916  	}
   917  	for _, entry := range keptAtIdxs {
   918  		if newAtSignIndexesCount > 0 && !added && entry.From > lastNewIdx {
   919  			newAtIndexEntries = append(newAtIndexEntries, makeAtIdxs(newAtSignIndexes)...)
   920  			newAtIndexEntries = append(newAtIndexEntries, entry)
   921  			added = true
   922  		} else {
   923  			newAtIndexEntries = append(newAtIndexEntries, entry)
   924  		}
   925  	}
   926  	if !added {
   927  		newAtIndexEntries = append(newAtIndexEntries, makeAtIdxs(newAtSignIndexes)...)
   928  	}
   929  	return newAtIndexEntries
   930  }
   931  
   932  func makeAtIdxs(idxs []int) []*AtIndexEntry {
   933  	result := make([]*AtIndexEntry, len(idxs))
   934  	for i, idx := range idxs {
   935  		result[i] = &AtIndexEntry{
   936  			From:    idx,
   937  			Checked: false,
   938  		}
   939  	}
   940  	return result
   941  }
   942  
   943  // getAtSignIdxs returns the indexes of all @ signs in the text.
   944  // delta is the offset of the text within the original text.
   945  func getAtSignIdxs(text string, delta int) []int {
   946  	return getAtSignIdxsHelper(text, delta, 0, []int{})
   947  }
   948  
   949  func getAtSignIdxsHelper(text string, delta int, from int, idxs []int) []int {
   950  	tr := []rune(text)
   951  	idx := strings.IndexRune(string(tr[from:]), '@')
   952  	if idx != -1 {
   953  		idx = utf8.RuneCountInString(text[:idx])
   954  		idx += from
   955  		idxs = append(idxs, delta+idx)
   956  		return getAtSignIdxsHelper(text, delta, idx+1, idxs)
   957  	}
   958  	return idxs
   959  }
   960  
   961  func checkAtIndexEntry(fullText string, entry *AtIndexEntry, mentionableUsers map[string]*MentionableUser) *AtIndexEntry {
   962  	if entry.Checked {
   963  		return entry
   964  	}
   965  	result := MatchMention(fullText, mentionableUsers, entry.From)
   966  	if result != nil && result.Match != "" {
   967  		return &AtIndexEntry{
   968  			From:      entry.From,
   969  			To:        entry.From + len([]rune(result.Match)),
   970  			Checked:   true,
   971  			Mentioned: true,
   972  		}
   973  	}
   974  	return &AtIndexEntry{
   975  		From:    entry.From,
   976  		To:      len([]rune(fullText)),
   977  		Checked: true,
   978  	}
   979  }
   980  
   981  func checkIdxForMentions(fullText string, currentAtIndexEntries []*AtIndexEntry, mentionableUsers map[string]*MentionableUser) []*AtIndexEntry {
   982  	var newIndexEntries []*AtIndexEntry
   983  	for _, entry := range currentAtIndexEntries {
   984  		previousEntryIdx := len(newIndexEntries) - 1
   985  		newEntry := checkAtIndexEntry(fullText, entry, mentionableUsers)
   986  		if previousEntryIdx >= 0 && !newIndexEntries[previousEntryIdx].Mentioned {
   987  			newIndexEntries[previousEntryIdx].To = entry.From - 1
   988  		}
   989  		if previousEntryIdx >= 0 {
   990  			newIndexEntries[previousEntryIdx].NextAtIdx = entry.From
   991  		}
   992  		newEntry.NextAtIdx = intUnknown
   993  		newIndexEntries = append(newIndexEntries, newEntry)
   994  	}
   995  
   996  	if len(newIndexEntries) > 0 {
   997  		lastIdx := len(newIndexEntries) - 1
   998  		if newIndexEntries[lastIdx].Mentioned {
   999  			return newIndexEntries
  1000  		}
  1001  		newIndexEntries[lastIdx].To = len([]rune(fullText)) - 1
  1002  		newIndexEntries[lastIdx].Checked = false
  1003  		return newIndexEntries
  1004  	}
  1005  
  1006  	return nil
  1007  }
  1008  
  1009  func appendInputSegment(result *[]InputSegment, typ SegmentType, value string, fullText *string) {
  1010  	if value != "" {
  1011  		*result = append(*result, InputSegment{Type: typ, Value: value})
  1012  		*fullText += value
  1013  	}
  1014  }
  1015  
  1016  func calculateInput(text string, atIndexEntries []*AtIndexEntry) ([]InputSegment, bool) {
  1017  	if len(atIndexEntries) == 0 {
  1018  		return []InputSegment{{Type: Text, Value: text}}, true
  1019  	}
  1020  	idxCount := len(atIndexEntries)
  1021  	lastFrom := atIndexEntries[idxCount-1].From
  1022  
  1023  	var result []InputSegment
  1024  	fullText := ""
  1025  	if atIndexEntries[0].From != 0 {
  1026  		t := subs(text, 0, atIndexEntries[0].From)
  1027  		appendInputSegment(&result, Text, t, &fullText)
  1028  	}
  1029  
  1030  	for _, entry := range atIndexEntries {
  1031  		from := entry.From
  1032  		to := entry.To
  1033  		nextAtIdx := entry.NextAtIdx
  1034  		mentioned := entry.Mentioned
  1035  
  1036  		if mentioned && nextAtIdx != intUnknown {
  1037  			t := subs(text, from, to+1)
  1038  			appendInputSegment(&result, Mention, t, &fullText)
  1039  
  1040  			t = subs(text, to+1, nextAtIdx)
  1041  			appendInputSegment(&result, Text, t, &fullText)
  1042  		} else if mentioned && lastFrom == from {
  1043  			t := subs(text, from, to+1)
  1044  			appendInputSegment(&result, Mention, t, &fullText)
  1045  
  1046  			t = subs(text, to+1)
  1047  			appendInputSegment(&result, Text, t, &fullText)
  1048  		} else {
  1049  			t := subs(text, from, to+1)
  1050  			appendInputSegment(&result, Text, t, &fullText)
  1051  		}
  1052  	}
  1053  
  1054  	return result, fullText == text
  1055  }
  1056  
  1057  func subs(s string, start int, end ...int) string {
  1058  	tr := []rune(s)
  1059  	e := len(tr)
  1060  
  1061  	if len(end) > 0 {
  1062  		e = end[0]
  1063  	}
  1064  
  1065  	if start < 0 {
  1066  		start = 0
  1067  	}
  1068  
  1069  	if e > len(tr) {
  1070  		e = len(tr)
  1071  	}
  1072  
  1073  	if e < 0 {
  1074  		e = 0
  1075  	}
  1076  
  1077  	if start > e {
  1078  		start, e = e, start
  1079  		if e > len(tr) {
  1080  			e = len(tr)
  1081  		}
  1082  	}
  1083  
  1084  	return string(tr[start:e])
  1085  }
  1086  
  1087  func isValidTerminatingCharacter(c rune) bool {
  1088  	switch c {
  1089  	case '\t': // tab
  1090  		return true
  1091  	case '\n': // newline
  1092  		return true
  1093  	case '\f': // new page
  1094  		return true
  1095  	case '\r': // carriage return
  1096  		return true
  1097  	case ' ': // whitespace
  1098  		return true
  1099  	case ',':
  1100  		return true
  1101  	case '.':
  1102  		return true
  1103  	case ':':
  1104  		return true
  1105  	case ';':
  1106  		return true
  1107  	default:
  1108  		return false
  1109  	}
  1110  }
  1111  
  1112  var hexReg = regexp.MustCompile("[0-9a-f]")
  1113  
  1114  func isPublicKeyCharacter(c rune) bool {
  1115  	return hexReg.MatchString(string(c))
  1116  }
  1117  
  1118  const mentionLength = 133
  1119  
  1120  func toInputField(text string) []InputSegment {
  1121  	// Initialize the variables
  1122  	currentMentionLength := 0
  1123  	currentText := ""
  1124  	currentMention := ""
  1125  	var inputFieldEntries []InputSegment
  1126  
  1127  	// Iterate through each character in the input text
  1128  	for _, character := range text {
  1129  		isPKCharacter := isPublicKeyCharacter(character)
  1130  		isTerminationCharacter := isValidTerminatingCharacter(character)
  1131  
  1132  		switch {
  1133  		// It's a valid mention.
  1134  		// Add any text that is before if present
  1135  		// and add the mention.
  1136  		// Set the text to the new termination character
  1137  		case currentMentionLength == mentionLength && isTerminationCharacter:
  1138  			if currentText != "" {
  1139  				inputFieldEntries = append(inputFieldEntries, InputSegment{Type: Text, Value: currentText})
  1140  			}
  1141  			inputFieldEntries = append(inputFieldEntries, InputSegment{Type: Mention, Value: currentMention})
  1142  			currentMentionLength = 0
  1143  			currentMention = ""
  1144  			currentText = string(character)
  1145  
  1146  		// It's either a pk character, or the `x` in the pk
  1147  		// in this case add the text to the mention and continue
  1148  		case (isPKCharacter && currentMentionLength > 0) || (currentMentionLength == 2 && character == 'x'):
  1149  			currentMentionLength++
  1150  			currentMention += string(character)
  1151  
  1152  		// The beginning of a mention, discard the @ sign
  1153  		// and start following a mention
  1154  		case character == '@':
  1155  			currentMentionLength = 1
  1156  			currentMention = ""
  1157  
  1158  		// Not a mention character, but we were following a mention
  1159  		// discard everything up to now and count as text
  1160  		case !isPKCharacter && currentMentionLength > 0:
  1161  			currentText += "@" + currentMention + string(character)
  1162  			currentMentionLength = 0
  1163  			currentMention = ""
  1164  
  1165  		// Just a normal text character
  1166  		default:
  1167  			currentText += string(character)
  1168  		}
  1169  	}
  1170  
  1171  	// Process any remaining mention/text
  1172  	if currentText != "" {
  1173  		inputFieldEntries = append(inputFieldEntries, InputSegment{Type: Text, Value: currentText})
  1174  	}
  1175  	if currentMentionLength == mentionLength {
  1176  		inputFieldEntries = append(inputFieldEntries, InputSegment{Type: Mention, Value: currentMention})
  1177  	}
  1178  
  1179  	return inputFieldEntries
  1180  }
  1181  
  1182  func toInfo(inputSegments []InputSegment) *MentionState {
  1183  	newText := ""
  1184  	state := &MentionState{
  1185  		AtSignIdx:    intUnknown,
  1186  		End:          intUnknown,
  1187  		AtIdxs:       []*AtIndexEntry{},
  1188  		MentionEnd:   0,
  1189  		PreviousText: "",
  1190  		NewText:      newText,
  1191  		Start:        intUnknown,
  1192  	}
  1193  
  1194  	for _, segment := range inputSegments {
  1195  		t := segment.Type
  1196  		text := segment.Value
  1197  		tr := []rune(text)
  1198  
  1199  		if t == Mention {
  1200  			newMention := &AtIndexEntry{
  1201  				Checked:   true,
  1202  				Mentioned: true,
  1203  				From:      state.MentionEnd,
  1204  				To:        state.Start + len(tr),
  1205  			}
  1206  
  1207  			if len(state.AtIdxs) > 0 {
  1208  				lastIdx := state.AtIdxs[len(state.AtIdxs)-1]
  1209  				state.AtIdxs = state.AtIdxs[:len(state.AtIdxs)-1]
  1210  				lastIdx.NextAtIdx = state.MentionEnd
  1211  				state.AtIdxs = append(state.AtIdxs, lastIdx)
  1212  			}
  1213  			state.AtIdxs = append(state.AtIdxs, newMention)
  1214  			state.AtSignIdx = state.MentionEnd
  1215  		}
  1216  
  1217  		state.MentionEnd += len(tr)
  1218  		state.NewText = string(tr[len(tr)-1])
  1219  		state.Start += len(tr)
  1220  		state.End += len(tr)
  1221  	}
  1222  
  1223  	return state
  1224  }
  1225  
  1226  // lastIndexOfAtSign returns the index of the last occurrence of substr in s starting from index start.
  1227  // If substr is not present in s, it returns -1.
  1228  func lastIndexOfAtSign(s string, start int) int {
  1229  	if start < 0 {
  1230  		return -1
  1231  	}
  1232  
  1233  	t := []rune(s)
  1234  	if start >= len(t) {
  1235  		start = len(t) - 1
  1236  	}
  1237  
  1238  	// Reverse the input strings to find the first occurrence of the reversed substr in the reversed s.
  1239  	reversedS := reverse(t[:start+1])
  1240  
  1241  	idx := strings.IndexRune(reversedS, '@')
  1242  
  1243  	if idx == -1 {
  1244  		return -1
  1245  	}
  1246  
  1247  	// Calculate the index in the original string.
  1248  	idx = utf8.RuneCountInString(reversedS[:idx])
  1249  	return start - idx
  1250  }
  1251  
  1252  // reverse returns the reversed string of input s.
  1253  func reverse(r []rune) string {
  1254  	for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
  1255  		r[i], r[j] = r[j], r[i]
  1256  	}
  1257  	return string(r)
  1258  }
  1259  
  1260  type textOperation int
  1261  
  1262  const (
  1263  	textOperationAdd textOperation = iota + 1
  1264  	textOperationDelete
  1265  	textOperationReplace
  1266  )
  1267  
  1268  type TextDiff struct {
  1269  	previousText string
  1270  	newText      string // if add operation, newText is the added text; if replace operation, newText is the text used to replace the previousText
  1271  	start        int    // start index of the operation relate to previousText
  1272  	end          int    // end index of the operation relate to previousText, always the same as start if the operation is add, range: start<=end<=len(previousText)-1
  1273  	operation    textOperation
  1274  }
  1275  
  1276  func diffText(oldText, newText string) *TextDiff {
  1277  	if oldText == newText {
  1278  		return nil
  1279  	}
  1280  	t1 := []rune(oldText)
  1281  	t2 := []rune(newText)
  1282  	oldLen := len(t1)
  1283  	newLen := len(t2)
  1284  	if oldLen == 0 {
  1285  		return &TextDiff{previousText: oldText, newText: newText, start: 0, end: 0, operation: textOperationAdd}
  1286  	}
  1287  	if newLen == 0 {
  1288  		return &TextDiff{previousText: oldText, newText: "", start: 0, end: oldLen, operation: textOperationReplace}
  1289  	}
  1290  
  1291  	// if we reach here, t1 and t2 are not empty
  1292  	start := 0
  1293  	for start < oldLen && start < newLen && t1[start] == t2[start] {
  1294  		start++
  1295  	}
  1296  
  1297  	oldEnd, newEnd := oldLen, newLen
  1298  	for oldEnd > start && newEnd > start && t1[oldEnd-1] == t2[newEnd-1] {
  1299  		oldEnd--
  1300  		newEnd--
  1301  	}
  1302  
  1303  	diff := &TextDiff{previousText: oldText, start: start}
  1304  	if newLen > oldLen && (start == oldLen || oldEnd == 0 || start == oldEnd) {
  1305  		diff.operation = textOperationAdd
  1306  		diff.end = start
  1307  		diff.newText = string(t2[start:newEnd])
  1308  	} else if newLen < oldLen && (start == newLen || newEnd == 0 || start == newEnd) {
  1309  		diff.operation = textOperationDelete
  1310  		diff.end = oldEnd - 1
  1311  	} else {
  1312  		diff.operation = textOperationReplace
  1313  		if start == 0 && oldEnd == oldLen { // full replace
  1314  			diff.end = oldLen - 1
  1315  			diff.newText = newText
  1316  		} else { // partial replace
  1317  			diff.end = oldEnd - 1
  1318  			diff.newText = string(t2[start:newEnd])
  1319  		}
  1320  	}
  1321  	return diff
  1322  }