public void OnDeleteTopic(Engine.Topics.Topic topic, IDObjects.NumberSet linksAffected, CodeDB.EventAccessor eventAccessor) { if (!IncludeInIndex(topic)) { return; } var entry = new SearchIndex.Entries.Topic(topic, this); accessLock.EnterWriteLock(); try { foreach (string keyword in entry.Keywords) { string prefix = KeywordPrefix(keyword); var numberSet = prefixTopicIDs[prefix]; if (numberSet != null) { numberSet.Remove(topic.TopicID); if (numberSet.IsEmpty) { prefixTopicIDs.Remove(prefix); foreach (var changeWatcher in changeWatchers) { changeWatcher.OnDeletePrefix(prefix, eventAccessor); } } else { foreach (var changeWatcher in changeWatchers) { changeWatcher.OnUpdatePrefix(prefix, eventAccessor); } } } } } finally { accessLock.ExitWriteLock(); } }
// Group: CodeDB.IChangeWatcher Functions // __________________________________________________________________________ public void OnAddTopic(Engine.Topics.Topic topic, CodeDB.EventAccessor eventAccessor) { if (!IncludeInIndex(topic)) { return; } var entry = new SearchIndex.Entries.Topic(topic, this); accessLock.EnterWriteLock(); try { foreach (string keyword in entry.Keywords) { string prefix = KeywordPrefix(keyword); var topicIDs = prefixTopicIDs[prefix]; if (topicIDs == null) { topicIDs = new IDObjects.NumberSet(); topicIDs.Add(topic.TopicID); prefixTopicIDs[prefix] = topicIDs; foreach (var changeWatcher in changeWatchers) { changeWatcher.OnAddPrefix(prefix, eventAccessor); } } else { topicIDs.Add(topic.TopicID); foreach (var changeWatcher in changeWatchers) { changeWatcher.OnUpdatePrefix(prefix, eventAccessor); } } } } finally { accessLock.ExitWriteLock(); } }
/* Function: IncludeInIndex * Whether the passed <Engine.Topics.Topic> should be included in the search index. */ protected bool IncludeInIndex(Engine.Topics.Topic topic) { var commentType = EngineInstance.CommentTypes.FromID(topic.CommentTypeID); if (commentType.ID == EngineInstance.CommentTypes.IDFromKeyword("group")) { return(false); } // If it's a code topic and it ends with a duplicate it's most likely a constructor. Don't index it since the user almost // certainly want the class and this just pollutes the results by forcing them under a keyword heading. else if (commentType.Flags.Code && commentType.Flags.ClassHierarchy == false && topic.Symbol.EndsWithDuplicate()) { return(false); } else { return(true); } }
/* Function: HashPath * * Returns the hash path for the topic. When appending this to the hash path of a file or class use a colon to separate * them. * * Examples: * * topic - Member * topic + includeClass - Module.Module.Member */ static public string HashPath(Engine.Topics.Topic topic, bool includeClass = true) { // We want to work from Topic.Title instead of Topic.Symbol so that we can use the separator characters as originally // written, as opposed to having them normalized and condensed in the anchor. int titleParametersIndex = ParameterString.GetParametersIndex(topic.Title); StringBuilder hashPath; if (titleParametersIndex == -1) { hashPath = new StringBuilder(topic.Title); } else { hashPath = new StringBuilder(titleParametersIndex); hashPath.Append(topic.Title, 0, titleParametersIndex); } hashPath.Replace('\t', ' '); // Remove all whitespace unless it separates two text characters. int i = 0; while (i < hashPath.Length) { if (hashPath[i] == ' ') { if (i == 0 || i == hashPath.Length - 1) { hashPath.Remove(i, 1); } else if (Tokenizer.FundamentalTypeOf(hashPath[i - 1]) == FundamentalType.Text && Tokenizer.FundamentalTypeOf(hashPath[i + 1]) == FundamentalType.Text) { i++; } else { hashPath.Remove(i, 1); } } else { i++; } } // Add parentheses to distinguish between multiple symbols in the same file. // xxx this will be a problem when doing class hash paths as symboldefnumber is only unique to a file if (topic.SymbolDefinitionNumber != 1) { hashPath.Append('('); hashPath.Append(topic.SymbolDefinitionNumber); hashPath.Append(')'); } // Add class if present and desired. // xxx when class id is included in topic test for that here, maybe instead of having a flag if (includeClass) { // Find the part of the symbol that isn't generated by the title, if any. string ignore; string titleSymbol = SymbolString.FromPlainText(topic.Title, out ignore).ToString(); string fullSymbol = topic.Symbol.ToString(); if (titleSymbol.Length < fullSymbol.Length && fullSymbol.Substring(fullSymbol.Length - titleSymbol.Length) == titleSymbol) { string classSymbol = fullSymbol.Substring(0, fullSymbol.Length - titleSymbol.Length); classSymbol = classSymbol.Replace(SymbolString.SeparatorChar, '.'); // The class symbol should already have a trailing member operator. hashPath.Insert(0, classSymbol); } } return(Utilities.Sanitize(hashPath.ToString())); }
// Group: Functions // __________________________________________________________________________ public Topic(Engine.Topics.Topic topic, SearchIndex.Manager searchIndex) : base() { wrappedTopic = topic; var commentType = searchIndex.EngineInstance.CommentTypes.FromID(topic.CommentTypeID); var language = searchIndex.EngineInstance.Languages.FromID(topic.LanguageID); // It's possible for language to be null if we're creating this to handle an OnDeleteTopic event for a language that // was deleted. var memberOperator = (language != null ? language.MemberOperator : "."); // Get the title without any parameters. We don't want to include parameters in the index. Multiple functions that // differ only by parameter will be treated as one entry. string title, ignore; ParameterString.SplitFromParameters(topic.Title, out title, out ignore); title = title.TrimEnd(); // Figure out the extra scope text that should be added to the title to make it a fully resolved symbol. We do this by // comparing the symbol from the topic to one generated from the title. We don't just use the symbol to begin with // because we want to show the title as written; there's some normalization that occurs when generating symbols // that we want to bypass. string extraScope = null; SymbolString titleSymbol = SymbolString.FromPlainText_NoParameters(title); string titleSymbolString = titleSymbol.FormatWithSeparator(memberOperator); string symbolString = topic.Symbol.FormatWithSeparator(memberOperator); if (symbolString.Length > titleSymbolString.Length) { // We have to go by LastIndexOf rather than EndsWith because "operator<string>" will have "<string>" cut off as a parameter. // We also have to go by LastIndexOf instead of IndexOf so constructors don't get cut off (Package.Class.Class). int titleIndex = symbolString.LastIndexOf(titleSymbolString); #if DEBUG if (titleIndex == -1) { throw new Exception("Title symbol string \"" + titleSymbolString + "\" isn't part of symbol string \"" + symbolString + "\" which " + "was assumed when creating a search index entry."); } #endif extraScope = symbolString.Substring(0, titleIndex); } // Remove the space in "operator <". This prevents them from appearing as two keywords, and also makes sure "operator <" and // "operator<" are always displayed consistently, which will be important for sorting. title = SpaceAfterOperatorKeywordRegex.Replace(title, ""); displayName = (extraScope == null ? title : extraScope + title); searchText = Normalize(displayName); if (commentType.IsFile) { endOfDisplayNameQualifiers = FindEndOfQualifiers(displayName, FileSplitSymbolsRegex.Matches(displayName)); endOfSearchTextQualifiers = FindEndOfQualifiers(searchText, FileSplitSymbolsRegex.Matches(searchText)); } else if (commentType.IsCode) { endOfDisplayNameQualifiers = FindEndOfQualifiers(displayName, CodeSplitSymbolsRegex.Matches(displayName)); endOfSearchTextQualifiers = FindEndOfQualifiers(searchText, CodeSplitSymbolsRegex.Matches(searchText)); } else // documentation topic { if (extraScope == null) { endOfDisplayNameQualifiers = 0; endOfSearchTextQualifiers = 0; } else { endOfDisplayNameQualifiers = extraScope.Length; // Don't need +1 because only leading separators are removed. The trailing separator will still be there. endOfSearchTextQualifiers = Normalize(extraScope).Length; } } keywords = new List <string>(); if (endOfDisplayNameQualifiers == 0) { AddKeywords(displayName, isDocumentation: commentType.IsDocumentation); } else { AddKeywords(displayName.Substring(endOfDisplayNameQualifiers), isDocumentation: commentType.IsDocumentation); } }
public void OnUpdateTopic(Engine.Topics.Topic oldTopic, Engine.Topics.Topic newTopic, Engine.Topics.Topic.ChangeFlags changeFlags, CodeDB.EventAccessor eventAccessor) { #if DEBUG if (IncludeInIndex(newTopic) != IncludeInIndex(oldTopic)) { throw new Exception("SearchIndex incorrectly assumes IncludeInIndex() will be the same for both the old and new topics in OnUpdateTopic()."); } #endif if (!IncludeInIndex(newTopic)) { return; } if ((changeFlags & (Engine.Topics.Topic.ChangeFlags.Title | Engine.Topics.Topic.ChangeFlags.CommentTypeID | Engine.Topics.Topic.ChangeFlags.SymbolDefinitonNumber | Engine.Topics.Topic.ChangeFlags.Symbol | Engine.Topics.Topic.ChangeFlags.LanguageID | Engine.Topics.Topic.ChangeFlags.FileID | Engine.Topics.Topic.ChangeFlags.EffectiveAccessLevel | Engine.Topics.Topic.ChangeFlags.Class)) == 0) { return; } // We assume that if the topics are similar enough to use OnUpdateTopic() instead of OnAdd/RemoveTopic() then they'll generate the exact // same keyword list, and they'll even be in the same order. This allows for a nice optimization here, but test it in debug builds in case these // assumptions are wrong in the future. #if DEBUG if (newTopic.TopicID != oldTopic.TopicID) { throw new Exception("SearchIndex incorrectly assumes both the old and new topics in OnUpdateTopic() have the same topic IDs."); } var newEntry = new SearchIndex.Entries.Topic(newTopic, this); var oldEntry = new SearchIndex.Entries.Topic(oldTopic, this); if (newEntry.Keywords.Count != oldEntry.Keywords.Count) { throw new Exception("SearchIndex incorrectly assumes both the old and new topics in OnUpdateTopic() have the same keywords."); } for (int i = 0; i < newEntry.Keywords.Count; i++) { if (newEntry.Keywords[i] != oldEntry.Keywords[i]) { throw new Exception("SearchIndex incorrectly assumes both the old and new topics in OnUpdateTopic() have the same keywords."); } } #endif var entry = new SearchIndex.Entries.Topic(newTopic, this); // We use upgradeable in case one of the change handlers needs to do something that requires a write lock. accessLock.EnterUpgradeableReadLock(); try { foreach (var keyword in entry.Keywords) { string prefix = KeywordPrefix(keyword); foreach (var changeWatcher in changeWatchers) { changeWatcher.OnUpdatePrefix(prefix, eventAccessor); } } } finally { // We don't have to test to see if it was upgraded because if it was the write lock should have been released // by the change handler and we should recursively be back to an upgradeable read lock. accessLock.ExitUpgradeableReadLock(); } }