void CMP_TREE_NODE_LIB_ID::UpdateScore( EDA_COMBINED_MATCHER& aMatcher ) { if( Score <= 0 ) return; // Leaf nodes without scores are out of the game. if( !SearchTextNormalized ) { SearchText = SearchText.Lower(); SearchTextNormalized = true; } // Keywords and description we only count if the match string is at // least two characters long. That avoids spurious, low quality // matches. Most abbreviations are at three characters long. int found_pos = EDA_PATTERN_NOT_FOUND; int matchers_fired = 0; if( aMatcher.GetPattern() == MatchName ) { Score += 1000; // exact match. High score :) } else if( aMatcher.Find( MatchName, matchers_fired, found_pos ) ) { // Substring match. The earlier in the string the better. Score += matchPosScore( found_pos, 20 ) + 20; } else if( aMatcher.Find( Parent->MatchName, matchers_fired, found_pos ) ) { Score += 19; // parent name matches. score += 19 } else if( aMatcher.Find( SearchText, matchers_fired, found_pos ) ) { // If we have a very short search term (like one or two letters), // we don't want to accumulate scores if they just happen to be in // keywords or description as almost any one or two-letter // combination shows up in there. if( aMatcher.GetPattern().length() >= 2 ) { // For longer terms, we add scores 1..18 for positional match // (higher in the front, where the keywords are). Score += matchPosScore( found_pos, 17 ) + 1; } } else { // No match. That's it for this item. Score = 0; } // More matchers = better match Score += 2 * matchers_fired; }
void COMPONENT_TREE_SEARCH_CONTAINER::UpdateSearchTerm( const wxString& aSearch ) { if( m_tree == NULL ) return; //#define SHOW_CALC_TIME // uncomment this to show calculation time #ifdef SHOW_CALC_TIME unsigned starttime = GetRunningMicroSecs(); #endif // We score the list by going through it several time, essentially with a complexity // of O(n). For the default library of 2000+ items, this typically takes less than 5ms // on an i5. Good enough, no index needed. // Initial AND condition: Leaf nodes are considered to match initially. for( TREE_NODE* node : m_nodes ) { node->PreviousScore = node->MatchScore; node->MatchScore = ( node->Type == TREE_NODE::TYPE_LIB ) ? 0 : kLowestDefaultScore; } // Create match scores for each node for all the terms, that come space-separated. // Scoring adds up values for each term according to importance of the match. If a term does // not match at all, the result is thrown out of the results (AND semantics). // From high to low // - Exact match for a ccmponent name gives the highest score, trumping all. // - A positional score depending of where a term is found as substring; prefix-match: high. // - substring-match in library name. // - substring match in keywords and descriptions with positional score. Keywords come // first so contribute more to the score. // // This is of course subject to tweaking. wxStringTokenizer tokenizer( aSearch ); while ( tokenizer.HasMoreTokens() ) { const wxString term = tokenizer.GetNextToken().Lower(); EDA_COMBINED_MATCHER matcher( term ); for( TREE_NODE* node : m_nodes ) { if( node->Type != TREE_NODE::TYPE_ALIAS ) continue; // Only aliases are actually scored here. if( node->MatchScore == 0) continue; // Leaf node without score are out of the game. // Keywords and description we only count if the match string is at // least two characters long. That avoids spurious, low quality // matches. Most abbreviations are at three characters long. int found_pos; int matcher_fired = 0; if( term == node->MatchName ) node->MatchScore += 1000; // exact match. High score :) else if( (found_pos = matcher.Find( node->MatchName, &matcher_fired ) ) != EDA_PATTERN_NOT_FOUND ) { // Substring match. The earlier in the string the better. score += 20..40 node->MatchScore += matchPosScore( found_pos, 20 ) + 20; } else if( matcher.Find( node->Parent->MatchName, &matcher_fired ) != EDA_PATTERN_NOT_FOUND ) node->MatchScore += 19; // parent name matches. score += 19 else if( ( found_pos = matcher.Find( node->SearchText, &matcher_fired ) ) != EDA_PATTERN_NOT_FOUND ) { // If we have a very short search term (like one or two letters), we don't want // to accumulate scores if they just happen to be in keywords or description as // almost any one or two-letter combination shows up in there. // For longer terms, we add scores 1..18 for positional match (higher in the // front, where the keywords are). score += 0..18 node->MatchScore += ( ( term.length() >= 2 ) ? matchPosScore( found_pos, 17 ) + 1 : 0 ); } else node->MatchScore = 0; // No match. That's it for this item. node->MatchScore += 2 * matcher_fired; } } // Library nodes have the maximum score seen in any of their children. // Alias nodes have the score of their parents. unsigned highest_score_seen = 0; bool any_change = false; for( TREE_NODE* node : m_nodes ) { switch( node->Type ) { case TREE_NODE::TYPE_ALIAS: { any_change |= (node->PreviousScore != node->MatchScore); // Update library score. node->Parent->MatchScore = std::max( node->Parent->MatchScore, node->MatchScore ); highest_score_seen = std::max( highest_score_seen, node->MatchScore ); } break; case TREE_NODE::TYPE_UNIT: node->MatchScore = node->Parent->MatchScore; break; default: break; } } // The tree update might be slow, so we want to bail out if there is no change. if( !any_change ) return; // Now: sort all items according to match score, libraries first. std::sort( m_nodes.begin(), m_nodes.end(), scoreComparator ); #ifdef SHOW_CALC_TIME unsigned sorttime = GetRunningMicroSecs(); #endif // Fill the tree with all items that have a match. Re-arranging, adding and removing changed // items is pretty complex, so we just re-build the whole tree. m_tree->Freeze(); m_tree->DeleteAllItems(); const wxTreeItemId root_id = m_tree->AddRoot( wxEmptyString ); const TREE_NODE* first_match = NULL; const TREE_NODE* preselected_node = NULL; for( TREE_NODE* node : m_nodes ) { if( node->MatchScore == 0 ) continue; // If we have nodes that go beyond the default score, suppress nodes that // have the default score. That can happen if they have an honary += 0 score due to // some one-letter match in the keyword or description. In this case, we prefer matches // that just have higher scores. Improves relevancy and performance as the tree has to // display less items. if( highest_score_seen > kLowestDefaultScore && node->MatchScore == kLowestDefaultScore ) continue; wxString node_text; #if 0 // Node text with scoring information for debugging node_text.Printf( wxT("%s (s=%u)%s"), GetChars(node->DisplayName), node->MatchScore, GetChars( node->DisplayInfo )); #else node_text = node->DisplayName + node->DisplayInfo; #endif node->TreeId = m_tree->AppendItem( node->Parent ? node->Parent->TreeId : root_id, node_text ); // If we are a nicely scored alias, we want to have it visible. Also, if there // is only a single library in this container, we want to have it unfolded // (example: power library). if( node->Type == TREE_NODE::TYPE_ALIAS && ( node->MatchScore > kLowestDefaultScore || m_libraries_added == 1 ) ) { m_tree->Expand( node->TreeId ); if( first_match == NULL ) first_match = node; // First, highest scoring: the "I am feeling lucky" element. } // The first node that matches our pre-select criteria is choosen. 'First node' // means, it shows up in the history, as the history node is displayed very first // (by virtue of alphabetical ordering) if( preselected_node == NULL && node->Type == TREE_NODE::TYPE_ALIAS && node->MatchName == m_preselect_node_name ) preselected_node = node; // Refinement in case we come accross a matching unit node. if( preselected_node != NULL && preselected_node->Type == TREE_NODE::TYPE_ALIAS && node->Parent == preselected_node && m_preselect_unit_number >= 1 && node->Unit == m_preselect_unit_number ) preselected_node = node; } if( first_match ) // Highest score search match pre-selected. { m_tree->SelectItem( first_match->TreeId ); m_tree->EnsureVisible( first_match->TreeId ); } else if( preselected_node ) // No search, so history item preselected. { m_tree->SelectItem( preselected_node->TreeId ); m_tree->EnsureVisible( preselected_node->TreeId ); } m_tree->Thaw(); #ifdef SHOW_CALC_TIME unsigned endtime = GetRunningMicroSecs(); wxLogMessage( wxT("sort components %.1f ms, rebuild tree %.1f ms"), double(sorttime-starttime)/1000.0, double(endtime-sorttime)/1000.0 ); #endif }