FEATURE: add `f` and `t` search shortcuts for first post / title

FEATURE: add f and t search shortcuts for first post / title

Previously with had in:title and in:first search shortcuts for searching in first post or title only. They are a bit of handful to type.

This add 2 shortcuts (t and f) for searching titles of first posts.

This commit also cleans up all advanced filters, they were not properly regex terminated allowing for weird clauses like in:firstinator acting the same as in:first

diff --git a/lib/search.rb b/lib/search.rb
index 04f7315..9ec3006 100644
--- a/lib/search.rb
+++ b/lib/search.rb
@@ -253,43 +253,43 @@ class Search
     @advanced_filters
   end
 
-  advanced_filter(/status:open/) do |posts|
+  advanced_filter(/^status:open$/) do |posts|
     posts.where('NOT topics.closed AND NOT topics.archived')
   end
 
-  advanced_filter(/status:closed/) do |posts|
+  advanced_filter(/^status:closed$/) do |posts|
     posts.where('topics.closed')
   end
 
-  advanced_filter(/status:archived/) do |posts|
+  advanced_filter(/^status:archived$/) do |posts|
     posts.where('topics.archived')
   end
 
-  advanced_filter(/status:noreplies/) do |posts|
+  advanced_filter(/^status:noreplies$/) do |posts|
     posts.where("topics.posts_count = 1")
   end
 
-  advanced_filter(/status:single_user/) do |posts|
+  advanced_filter(/^status:single_user$/) do |posts|
     posts.where("topics.participant_count = 1")
   end
 
-  advanced_filter(/posts_count:(\d+)/) do |posts, match|
+  advanced_filter(/^posts_count:(\d+)$/) do |posts, match|
     posts.where("topics.posts_count = ?", match.to_i)
   end
 
-  advanced_filter(/min_post_count:(\d+)/) do |posts, match|
+  advanced_filter(/^min_post_count:(\d+)$/) do |posts, match|
     posts.where("topics.posts_count >= ?", match.to_i)
   end
 
-  advanced_filter(/in:first/) do |posts|
+  advanced_filter(/^in:first|^f$/) do |posts|
     posts.where("posts.post_number = 1")
   end
 
-  advanced_filter(/in:pinned/) do |posts|
+  advanced_filter(/^in:pinned$/) do |posts|
     posts.where("topics.pinned_at IS NOT NULL")
   end
 
-  advanced_filter(/in:unpinned/) do |posts|
+  advanced_filter(/^in:unpinned$/) do |posts|
     if @guardian.user
       posts.where("topics.pinned_at IS NOT NULL AND topics.id IN (
                   SELECT topic_id FROM topic_users WHERE user_id = ? AND cleared_pinned_at IS NOT NULL
@@ -297,11 +297,11 @@ class Search
     end
   end
 
-  advanced_filter(/in:wiki/) do |posts, match|
+  advanced_filter(/^in:wiki$/) do |posts, match|
     posts.where(wiki: true)
   end
 
-  advanced_filter(/badge:(.*)/) do |posts, match|
+  advanced_filter(/^badge:(.*)$/) do |posts, match|
     badge_id = Badge.where('name ilike ? OR id = ?', match, match.to_i).pluck(:id).first
     if badge_id
       posts.where('posts.user_id IN (SELECT ub.user_id FROM user_badges ub WHERE ub.badge_id = ?)', badge_id)
@@ -310,7 +310,7 @@ class Search
     end
   end
 
-  advanced_filter(/in:(likes|bookmarks)/) do |posts, match|
+  advanced_filter(/^in:(likes|bookmarks)$/) do |posts, match|
     if @guardian.user
       post_action_type = PostActionType.types[:like] if match == "likes"
       post_action_type = PostActionType.types[:bookmark] if match == "bookmarks"
@@ -324,11 +324,11 @@ class Search
     end
   end
 
-  advanced_filter(/in:posted/) do |posts|
+  advanced_filter(/^in:posted$/) do |posts|
     posts.where("posts.user_id = #{@guardian.user.id}") if @guardian.user
   end
 
-  advanced_filter(/in:(watching|tracking)/) do |posts, match|
+  advanced_filter(/^in:(watching|tracking)$/) do |posts, match|
     if @guardian.user
       level = TopicUser.notification_levels[match.to_sym]
       posts.where("posts.topic_id IN (
@@ -340,7 +340,7 @@ class Search
     end
   end
 
-  advanced_filter(/in:seen/) do |posts|
+  advanced_filter(/^in:seen$/) do |posts|
     if @guardian.user
       posts
         .joins("INNER JOIN post_timings ON
@@ -351,7 +351,7 @@ class Search
     end
   end
 
-  advanced_filter(/in:unseen/) do |posts|
+  advanced_filter(/^in:unseen$/) do |posts|
     if @guardian.user
       posts
         .joins("LEFT JOIN post_timings ON
@@ -363,11 +363,11 @@ class Search
     end
   end
 
-  advanced_filter(/with:images/) do |posts|
+  advanced_filter(/^with:images$/) do |posts|
     posts.where("posts.image_url IS NOT NULL")
   end
 
-  advanced_filter(/category:(.+)/) do |posts, match|
+  advanced_filter(/^category:(.+)$/) do |posts, match|
     exact = false
 
     if match[0] == "="
@@ -390,7 +390,7 @@ class Search
     end
   end
 
-  advanced_filter(/^\#([\p{L}0-9\-:=]+)/) do |posts, match|
+  advanced_filter(/^\#([\p{L}0-9\-:=]+)$/) do |posts, match|
 
     exact = true
 
@@ -443,7 +443,7 @@ class Search
     end
   end
 
-  advanced_filter(/group:(.+)/) do |posts, match|
+  advanced_filter(/^group:(.+)$/) do |posts, match|
     group_id = Group.where('name ilike ? OR (id = ? AND id > 0)', match, match.to_i).pluck(:id).first
     if group_id
       posts.where("posts.user_id IN (select gu.user_id from group_users gu where gu.group_id = ?)", group_id)
@@ -452,7 +452,7 @@ class Search
     end
   end
 
-  advanced_filter(/user:(.+)/) do |posts, match|
+  advanced_filter(/^user:(.+)$/) do |posts, match|
     user_id = User.where(staged: false).where('username_lower = ? OR id = ?', match.downcase, match.to_i).pluck(:id).first
     if user_id
       posts.where("posts.user_id = #{user_id}")
@@ -461,7 +461,7 @@ class Search
     end
   end
 
-  advanced_filter(/^\@([a-zA-Z0-9_\-.]+)/) do |posts, match|
+  advanced_filter(/^\@([a-zA-Z0-9_\-.]+)$/) do |posts, match|
     user_id = User.where(staged: false).where(username_lower: match.downcase).pluck(:id).first
     if user_id
       posts.where("posts.user_id = #{user_id}")
@@ -470,7 +470,7 @@ class Search
     end
   end
 
-  advanced_filter(/before:(.*)/) do |posts, match|
+  advanced_filter(/^before:(.*)$/) do |posts, match|
     if date = Search.word_to_date(match)
       posts.where("posts.created_at < ?", date)
     else
@@ -478,7 +478,7 @@ class Search
     end
   end
 
-  advanced_filter(/after:(.*)/) do |posts, match|
+  advanced_filter(/^after:(.*)$/) do |posts, match|
     if date = Search.word_to_date(match)
       posts.where("posts.created_at > ?", date)
     else
@@ -486,15 +486,15 @@ class Search
     end
   end
 
-  advanced_filter(/^tags?:([\p{L}0-9,\-_+]+)/) do |posts, match|
+  advanced_filter(/^tags?:([\p{L}0-9,\-_+]+)$/) do |posts, match|
     search_tags(posts, match, positive: true)
   end
 
-  advanced_filter(/\-tags?:([\p{L}0-9,\-_+]+)/) do |posts, match|
+  advanced_filter(/^\-tags?:([\p{L}0-9,\-_+]+)$/) do |posts, match|
     search_tags(posts, match, positive: false)
   end
 
-  advanced_filter(/filetypes?:([a-zA-Z0-9,\-_]+)/) do |posts, match|
+  advanced_filter(/^filetypes?:([a-zA-Z0-9,\-_]+)$/) do |posts, match|
     file_extensions = match.split(",").map(&:downcase)
     posts.where("posts.id IN (
       SELECT post_id
@@ -558,7 +558,7 @@ class Search
       elsif word == 'order:latest_topic'
         @order = :latest_topic
         nil
-      elsif word == 'in:title'
+      elsif word == 'in:title' || word == 't'
         @in_title = true
         nil
       elsif word =~ /topic:(\d+)/
diff --git a/spec/components/search_spec.rb b/spec/components/search_spec.rb
index 70b9ea8..159ba50 100644
--- a/spec/components/search_spec.rb
+++ b/spec/components/search_spec.rb
@@ -701,8 +701,14 @@ describe Search do
       expect(Search.execute('test after:jan').posts.length).to eq(1)
 
       expect(Search.execute('test in:first').posts.length).to eq(1)
+
       expect(Search.execute('boom').posts.length).to eq(1)
+
       expect(Search.execute('boom in:first').posts.length).to eq(0)
+      expect(Search.execute('boom f').posts.length).to eq(0)
+
+      expect(Search.execute('123 in:first').posts.length).to eq(1)
+      expect(Search.execute('123 f').posts.length).to eq(1)
 
       expect(Search.execute('user:nobody').posts.length).to eq(0)
       expect(Search.execute("user:#{_post.user.username}").posts.length).to eq(1)
@@ -1050,8 +1056,14 @@ describe Search do
       results = Search.execute('title in:title')
       expect(results.posts.length).to eq(1)
 
+      results = Search.execute('title t')
+      expect(results.posts.length).to eq(1)
+
       results = Search.execute('first in:title')
       expect(results.posts.length).to eq(0)
+
+      results = Search.execute('first t')
+      expect(results.posts.length).to eq(0)
     end
 
     it 'works irrespective of the order' do

GitHub sha: 0a357299

This commit has been mentioned on Discourse Meta. There might be relevant details there: