Quellcode durchsuchen

Help search less silly

pull/13/head
Rocketsoup vor 2 Monaten
Ursprung
Commit
36068989b8
1 geänderte Dateien mit 57 neuen und 50 gelöschten Zeilen
  1. 57
    50
      rocketbot/cogs/helpcog.py

+ 57
- 50
rocketbot/cogs/helpcog.py Datei anzeigen

@@ -1,6 +1,7 @@
1 1
 """Provides help commands for getting info on using other commands and configuration."""
2 2
 import re
3
-from typing import Union, Optional
3
+import time
4
+from typing import Union, Optional, TypedDict
4 5
 
5 6
 from discord import Interaction, Permissions, AppCommandType, ButtonStyle, InteractionResponse
6 7
 from discord.app_commands import Group, Command, autocomplete, guild_only, command, Choice
@@ -12,6 +13,10 @@ from rocketbot.cogs.basecog import BaseCog
12 13
 from rocketbot.utils import MOD_PERMISSIONS, dump_stacktrace
13 14
 
14 15
 HelpTopic = Union[Command, Group, BaseCog]
16
+class HelpMeta(TypedDict):
17
+	id: str
18
+	text: str
19
+	topic: HelpTopic
15 20
 
16 21
 # Potential place to break text neatly in large help content
17 22
 PAGE_BREAK = '\f'
@@ -39,7 +44,7 @@ async def search_autocomplete(interaction: Interaction, current: str) -> list[Ch
39 44
 		return [
40 45
 			choice_from_topic(topic, include_full_command=True)
41 46
 			for topic in HelpCog.shared.topics_for_keywords(current, interaction.permissions)
42
-		]
47
+		][:25]
43 48
 	except BaseException as e:
44 49
 		dump_stacktrace(e)
45 50
 		return []
@@ -57,53 +62,60 @@ class HelpCog(BaseCog, name='Help'):
57 62
 
58 63
 	def __create_help_index(self) -> None:
59 64
 		"""
60
-		Populates self.topic_index and self.keyword_index. Bails if already
65
+		Populates self.id_to_topic and self.keyword_index. Bails if already
61 66
 		populated. Intended to be run on demand so all cogs and commands have
62 67
 		had time to get set up and synced.
63 68
 		"""
64
-		if getattr(self, 'topic_index', None) is not None:
69
+		if getattr(self, 'id_to_topic', None) is not None:
65 70
 			return
66
-		self.topic_index: dict[str, HelpTopic] = {}
67
-		self.keyword_index: dict[str, set[HelpTopic]] = {}
71
+		self.id_to_topic: dict[str, HelpTopic] = {}
72
+		self.topics: list[HelpMeta] = []
68 73
 
69
-		def add_text_to_index(topic: HelpTopic, text: str):
70
-			words = [
74
+		def process_text(t: str) -> str:
75
+			return ' '.join([
71 76
 				word
72
-				for word in re.split(r"[^a-zA-Z']+", text.lower())
73
-				if len(word) > 1 and word not in trivial_words
74
-			]
75
-			for word in words:
76
-				matches = self.keyword_index.get(word, set())
77
-				matches.add(topic)
78
-				self.keyword_index[word] = matches
77
+				for word in re.split(r"[^a-z']+", t.lower())
78
+				if word not in trivial_words
79
+			]).strip()
79 80
 
80 81
 		cmds = self.all_commands()
81 82
 		for cmd in cmds:
82
-			self.topic_index[f'cmd:{cmd.name}'] = cmd
83
-			self.topic_index[f'/{cmd.name}'] = cmd
84
-			add_text_to_index(cmd, cmd.name)
83
+			key = f'cmd:{cmd.name}'
84
+			self.id_to_topic[key] = cmd
85
+			self.id_to_topic[f'/{cmd.name}'] = cmd
86
+			text = cmd.name
85 87
 			if cmd.description:
86
-				add_text_to_index(cmd, cmd.description)
88
+				text += f' {cmd.description}'
89
+			if cmd.extras.get('long_description', None):
90
+				text += f' {cmd.extras["long_description"]}'
91
+			self.topics.append({ 'id': key, 'text': process_text(text), 'topic': cmd })
87 92
 			if isinstance(cmd, Group):
88 93
 				for subcmd in cmd.commands:
89
-					self.topic_index[f'subcmd:{cmd.name}.{subcmd.name}'] = subcmd
90
-					self.topic_index[f'/{cmd.name} {subcmd.name}'] = subcmd
91
-					add_text_to_index(subcmd, cmd.name)
92
-					add_text_to_index(subcmd, subcmd.name)
94
+					key = f'subcmd:{cmd.name}.{subcmd.name}'
95
+					self.id_to_topic[key] = subcmd
96
+					self.id_to_topic[f'/{cmd.name} {subcmd.name}'] = subcmd
97
+					text = cmd.name
98
+					text += f' {subcmd.name}'
93 99
 					if subcmd.description:
94
-						add_text_to_index(subcmd, subcmd.description)
100
+						text += f' {subcmd.description}'
101
+					if subcmd.extras.get('long_description', None):
102
+						text += f' {subcmd.extras["long_description"]}'
103
+					self.topics.append({ 'id': key, 'text': process_text(text), 'topic': subcmd })
95 104
 		for cog_qname, cog in self.bot.cogs.items():
96 105
 			if not isinstance(cog, BaseCog):
97 106
 				continue
98 107
 			key = f'cog:{cog_qname}'
99
-			self.topic_index[key] = cog
100
-			add_text_to_index(cog, cog.qualified_name)
101
-			if cog.description:
102
-				add_text_to_index(cog, cog.description)
108
+			self.id_to_topic[key] = cog
109
+			text = cog.qualified_name
110
+			if cog.short_description:
111
+				text += f' {cog.short_description}'
112
+			if cog.long_description:
113
+				text += f' {cog.long_description}'
114
+			self.topics.append({ 'id': key, 'text': process_text(text), 'topic': cog })
103 115
 
104 116
 	def topic_for_help_symbol(self, symbol: str) -> Optional[HelpTopic]:
105 117
 		self.__create_help_index()
106
-		return self.topic_index.get(symbol, None)
118
+		return self.id_to_topic.get(symbol, None)
107 119
 
108 120
 	def all_commands(self) -> list[Union[Command, Group]]:
109 121
 		# PyCharm not interpreting conditional return type correctly.
@@ -149,46 +161,41 @@ class HelpCog(BaseCog, name='Help'):
149 161
 		return topics
150 162
 
151 163
 	def topics_for_keywords(self, search: str, permissions: Optional[Permissions]) -> list[HelpTopic]:
164
+		start_time = time.perf_counter()
152 165
 		self.__create_help_index()
153 166
 
154 167
 		# Break into words (or word fragments)
155 168
 		words: list[str] = [
156
-			word.lower()
157
-			for word in re.split(r"[^a-zA-Z']+", search)
158
-			# if len(word) > 1 and word not in trivial_words
169
+			word
170
+			for word in re.split(r"[^a-z']+", search.lower())
159 171
 		]
160 172
 
161
-		# FIXME: This is a super weird way of doing this. Converting word fragments
162
-		# to known indexed keywords, then collecting those associated results. Should
163
-		# just keep corpuses of searchable, normalized text for each topic and do a
164
-		# direct `in` test.
165
-		matching_topics_set = None
166
-		for word in words:
167
-			word_matches = set()
168
-			for k in self.keyword_index.keys():
169
-				if word in k:
170
-					topics = self.keyword_index.get(k, None)
171
-					if topics is not None:
172
-						word_matches.update(topics)
173
-			if matching_topics_set is None:
174
-				matching_topics_set = word_matches
175
-			else:
176
-				matching_topics_set = matching_topics_set & word_matches
173
+		# Find matches
174
+		def topic_matches(meta: HelpMeta) -> bool:
175
+			for word in words:
176
+				if word not in meta['text']:
177
+					return False
178
+			return True
179
+		matching_topics: list[HelpTopic] = [ topic['topic'] for topic in self.topics if topic_matches(topic) ]
177 180
 
178 181
 		# Filter by accessibility
179 182
 		accessible_topics = [
180 183
 			topic
181
-			for topic in matching_topics_set or {}
184
+			for topic in matching_topics
182 185
 			if ((isinstance(topic, Command) or isinstance(topic, Group)) and can_use_command(topic, permissions)) or \
183 186
 			   (isinstance(topic, BaseCog) and can_use_cog(topic, permissions))
184 187
 		]
185 188
 
186 189
 		# Sort and return
187
-		return sorted(accessible_topics, key=lambda topic: (
190
+		result = sorted(accessible_topics, key=lambda topic: (
188 191
 			isinstance(topic, Command),
189 192
 			isinstance(topic, BaseCog),
190 193
 			topic.qualified_name if isinstance(topic, BaseCog) else topic.name
191 194
 		))
195
+		duration = time.perf_counter() - start_time
196
+		if duration > 0.01:
197
+			self.log(None, f'search "{search}" took {duration} seconds')
198
+		return result
192 199
 
193 200
 	@command(
194 201
 		name='help',

Laden…
Abbrechen
Speichern