From 64f62d01412a07b46b3c4371f6a7946081f7351f Mon Sep 17 00:00:00 2001 From: Joshua Clayton Date: Wed, 30 Nov 2016 05:39:04 -0500 Subject: [PATCH] Allow for rg in addition to ag Why? ==== rg is oftentimes faster for searching across a codebase than ag; however, it's newer and potentially more unfamiliar, so ag still remains the default. This is an introduction to supporting rg, but without updating the Homebrew recipe yet. Given a trial run, I can imagine switching to it as a default eventually. --- README.md | 4 +++ app/App.hs | 9 +++++-- app/Main.hs | 16 ++++++++++++ src/Unused/CLI/Search.hs | 12 ++++----- src/Unused/TermSearch.hs | 29 ++++++++++++++++------ src/Unused/TermSearch/Internal.hs | 34 +++++++++++++++++--------- src/Unused/TermSearch/Types.hs | 3 +++ test/Unused/TermSearch/InternalSpec.hs | 29 ++++++++++++++-------- 8 files changed, 99 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index aaac61e..f5feaf8 100644 --- a/README.md +++ b/README.md @@ -363,6 +363,10 @@ Unused leverages [Ag](https://github.com/ggreer/the_silver_searcher) to analyze the codebase; as such, you'll need to have `ag` available in your `$PATH`. This is set as an explicit dependency in Homebrew. +Alternatively, if you'd like to use +[RipGrep](https://github.com/BurntSushi/ripgrep), you can do so with the +`--search rg` flag. Be sure to have RipGrep installed first. + ## Testing To run the test suite, run: diff --git a/app/App.hs b/app/App.hs index 55651fd..5c40739 100644 --- a/app/App.hs +++ b/app/App.hs @@ -21,7 +21,7 @@ import Unused.Parser (parseResults) import Unused.ResponseFilter (withOneOccurrence, withLikelihoods, ignoringPaths) import Unused.ResultsClassifier (ParseConfigError, LanguageConfiguration(..), loadAllConfigurations) import Unused.TagsSource (TagSearchOutcome, loadTagsFromFile, loadTagsFromPipe) -import Unused.TermSearch (SearchResults(..), SearchTerm, fromResults) +import Unused.TermSearch (SearchResults(..), SearchBackend(..), SearchTerm, fromResults) import Unused.Types (TermMatchSet, RemovalLikelihood(..)) type AppConfig = MonadReader Options @@ -45,6 +45,7 @@ data Options = Options , oWithoutCache :: Bool , oFromStdIn :: Bool , oCommitCount :: Maybe Int + , oSearchBackend :: SearchBackend } runProgram :: Options -> IO () @@ -57,10 +58,14 @@ run = do terms <- termsWithAlternatesFromConfig liftIO $ renderHeader terms - results <- withCache . (`executeSearch` terms) =<< searchRunner + backend <- searchBackend + results <- withCache . flip (executeSearch backend) terms =<< searchRunner printResults =<< retrieveGitContext =<< fmap (`parseResults` results) loadAllConfigs +searchBackend :: AppConfig m => m SearchBackend +searchBackend = asks oSearchBackend + termsWithAlternatesFromConfig :: App [SearchTerm] termsWithAlternatesFromConfig = do aliases <- concatMap lcTermAliases <$> loadAllConfigs diff --git a/app/Main.hs b/app/Main.hs index 15c8860..fa29b17 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -6,6 +6,7 @@ import qualified Data.Maybe as M import Options.Applicative import Unused.CLI (SearchRunner(..)) import Unused.Grouping (CurrentGrouping(..)) +import Unused.TermSearch (SearchBackend(..)) import Unused.Types (RemovalLikelihood(..)) import Unused.Util (stringToInt) @@ -38,6 +39,7 @@ parseOptions = <*> parseWithoutCache <*> parseFromStdIn <*> parseCommitCount + <*> parseSearchBackend parseSearchRunner :: Parser SearchRunner parseSearchRunner = @@ -116,3 +118,17 @@ parseCommitCount = commitParser = optional $ strOption $ long "commits" <> help "Number of recent commit SHAs to display per token" + +parseSearchBackend :: Parser SearchBackend +parseSearchBackend = M.fromMaybe Ag <$> maybeBackend + where + maybeBackend = optional $ parseBackend <$> parseBackendOption + parseBackendOption = + strOption $ + long "search" + <> help "[Allowed: ag, rg] Select searching backend" + +parseBackend :: String -> SearchBackend +parseBackend "ag" = Ag +parseBackend "rg" = Rg +parseBackend _ = Ag diff --git a/src/Unused/CLI/Search.hs b/src/Unused/CLI/Search.hs index db329c7..51a12a9 100644 --- a/src/Unused/CLI/Search.hs +++ b/src/Unused/CLI/Search.hs @@ -16,11 +16,11 @@ renderHeader terms = do U.resetScreen V.analysisHeader terms -executeSearch :: SearchRunner -> [TS.SearchTerm] -> IO TS.SearchResults -executeSearch runner terms = do +executeSearch :: TS.SearchBackend -> SearchRunner -> [TS.SearchTerm] -> IO TS.SearchResults +executeSearch backend runner terms = do renderHeader terms - runSearch runner terms <* U.resetScreen + runSearch backend runner terms <* U.resetScreen -runSearch :: SearchRunner -> [TS.SearchTerm] -> IO TS.SearchResults -runSearch SearchWithProgress = I.progressWithIndicator TS.search I.createProgressBar -runSearch SearchWithoutProgress = I.progressWithIndicator TS.search I.createSpinner +runSearch :: TS.SearchBackend -> SearchRunner -> [TS.SearchTerm] -> IO TS.SearchResults +runSearch b SearchWithProgress = I.progressWithIndicator (TS.search b) I.createProgressBar +runSearch b SearchWithoutProgress = I.progressWithIndicator (TS.search b) I.createSpinner diff --git a/src/Unused/TermSearch.hs b/src/Unused/TermSearch.hs index a49f45c..1289f5f 100644 --- a/src/Unused/TermSearch.hs +++ b/src/Unused/TermSearch.hs @@ -1,20 +1,33 @@ module Unused.TermSearch ( SearchResults(..) + , SearchBackend(..) , SearchTerm , search ) where import qualified Data.Maybe as M +import GHC.IO.Exception (ExitCode(ExitSuccess)) import qualified System.Process as P import Unused.TermSearch.Internal (commandLineOptions, parseSearchResult) -import Unused.TermSearch.Types (SearchResults(..)) +import Unused.TermSearch.Types (SearchResults(..), SearchBackend(..)) import Unused.Types (SearchTerm, searchTermToString) -search :: SearchTerm -> IO SearchResults -search t = - SearchResults . M.mapMaybe (parseSearchResult t) <$> (lines <$> ag (searchTermToString t)) +search :: SearchBackend -> SearchTerm -> IO SearchResults +search backend t = + SearchResults . M.mapMaybe (parseSearchResult backend t) <$> (lines <$> performSearch backend (searchTermToString t)) -ag :: String -> IO String -ag t = do - (_, results, _) <- P.readProcessWithExitCode "ag" (commandLineOptions t) "" - return results +performSearch :: SearchBackend -> String -> IO String +performSearch b t = extractSearchResults b <$> searchOutcome + where + searchOutcome = + P.readProcessWithExitCode + (backendToCommand b) + (commandLineOptions b t) + "" + backendToCommand Rg = "rg" + backendToCommand Ag = "ag" + +extractSearchResults :: SearchBackend -> (ExitCode, String, String) -> String +extractSearchResults Rg (ExitSuccess, stdout, _) = stdout +extractSearchResults Rg (_, _, stderr) = stderr +extractSearchResults Ag (_, stdout, _) = stdout diff --git a/src/Unused/TermSearch/Internal.hs b/src/Unused/TermSearch/Internal.hs index f880bdd..5d3bec5 100644 --- a/src/Unused/TermSearch/Internal.hs +++ b/src/Unused/TermSearch/Internal.hs @@ -6,26 +6,38 @@ module Unused.TermSearch.Internal import qualified Data.Char as C import qualified Data.Maybe as M import qualified Data.Text as T +import Unused.TermSearch.Types (SearchBackend(..)) import Unused.Types (SearchTerm(..), TermMatch(..)) import Unused.Util (stringToInt) -commandLineOptions :: String -> [String] -commandLineOptions t = +commandLineOptions :: SearchBackend -> String -> [String] +commandLineOptions backend t = if regexSafeTerm t - then ["(\\W|^)" ++ t ++ "(\\W|$)", "."] ++ baseFlags - else [t, ".", "-Q"] ++ baseFlags - where - baseFlags = ["-c", "--ackmate", "--ignore-dir", "tmp/unused"] + then regexFlags backend t ++ baseFlags backend + else nonRegexFlags backend t ++ baseFlags backend -parseSearchResult :: SearchTerm -> String -> Maybe TermMatch -parseSearchResult term = - maybeTermMatch . map T.unpack . T.splitOn ":" . T.pack +parseSearchResult :: SearchBackend -> SearchTerm -> String -> Maybe TermMatch +parseSearchResult backend term = + maybeTermMatch backend . map T.unpack . T.splitOn ":" . T.pack where - maybeTermMatch [_, path, count] = Just $ toTermMatch term path $ countInt count - maybeTermMatch _ = Nothing + maybeTermMatch Rg [path, count] = Just $ toTermMatch term path $ countInt count + maybeTermMatch Rg _ = Nothing + maybeTermMatch Ag [_, path, count] = Just $ toTermMatch term path $ countInt count + maybeTermMatch Ag _ = Nothing countInt = M.fromMaybe 0 . stringToInt toTermMatch (OriginalTerm t) path = TermMatch t path Nothing toTermMatch (AliasTerm t a) path = TermMatch t path (Just a) regexSafeTerm :: String -> Bool regexSafeTerm = all (\c -> C.isAlphaNum c || c == '_' || c == '-') + +nonRegexFlags :: SearchBackend -> String -> [String] +nonRegexFlags Rg t = [t, ".", "-F"] +nonRegexFlags Ag t = [t, ".", "-Q"] + +baseFlags :: SearchBackend -> [String] +baseFlags Rg = ["-c", "-j", "1"] +baseFlags Ag = ["-c", "--ackmate", "--ignore-dir", "tmp/unused"] + +regexFlags :: SearchBackend -> String -> [String] +regexFlags _ t = ["(\\W|^)" ++ t ++ "(\\W|$)", "."] diff --git a/src/Unused/TermSearch/Types.hs b/src/Unused/TermSearch/Types.hs index b6020a9..edb149e 100644 --- a/src/Unused/TermSearch/Types.hs +++ b/src/Unused/TermSearch/Types.hs @@ -2,8 +2,11 @@ module Unused.TermSearch.Types ( SearchResults(..) + , SearchBackend(..) ) where import Unused.Types (TermMatch) +data SearchBackend = Ag | Rg + newtype SearchResults = SearchResults { fromResults :: [TermMatch] } deriving (Monoid) diff --git a/test/Unused/TermSearch/InternalSpec.hs b/test/Unused/TermSearch/InternalSpec.hs index b801178..4fb43ff 100644 --- a/test/Unused/TermSearch/InternalSpec.hs +++ b/test/Unused/TermSearch/InternalSpec.hs @@ -5,6 +5,7 @@ module Unused.TermSearch.InternalSpec import Test.Hspec import Unused.TermSearch.Internal +import Unused.TermSearch.Types import Unused.Types main :: IO () @@ -14,17 +15,25 @@ spec :: Spec spec = parallel $ do describe "commandLineOptions" $ do it "does not use regular expressions when the term contains non-word characters" $ do - commandLineOptions "can_do_things?" `shouldBe` ["can_do_things?", ".", "-Q", "-c", "--ackmate", "--ignore-dir", "tmp/unused"] - commandLineOptions "no_way!" `shouldBe` ["no_way!", ".", "-Q", "-c", "--ackmate", "--ignore-dir", "tmp/unused"] - commandLineOptions "[]=" `shouldBe` ["[]=", ".", "-Q", "-c", "--ackmate", "--ignore-dir", "tmp/unused"] - commandLineOptions "window.globalOverride" `shouldBe` ["window.globalOverride", ".", "-Q", "-c", "--ackmate", "--ignore-dir", "tmp/unused"] + commandLineOptions Ag "can_do_things?" `shouldBe` ["can_do_things?", ".", "-Q", "-c", "--ackmate", "--ignore-dir", "tmp/unused"] + commandLineOptions Ag "no_way!" `shouldBe` ["no_way!", ".", "-Q", "-c", "--ackmate", "--ignore-dir", "tmp/unused"] + commandLineOptions Ag "[]=" `shouldBe` ["[]=", ".", "-Q", "-c", "--ackmate", "--ignore-dir", "tmp/unused"] + commandLineOptions Ag "window.globalOverride" `shouldBe` ["window.globalOverride", ".", "-Q", "-c", "--ackmate", "--ignore-dir", "tmp/unused"] - it "uses regular expression match with surrounding non-word matches for accuracy" $ - commandLineOptions "awesome_method" `shouldBe` ["(\\W|^)awesome_method(\\W|$)", ".", "-c", "--ackmate", "--ignore-dir", "tmp/unused"] + commandLineOptions Rg "can_do_things?" `shouldBe` ["can_do_things?", ".", "-F", "-c", "-j", "1"] + commandLineOptions Rg "no_way!" `shouldBe` ["no_way!", ".", "-F", "-c", "-j", "1"] + commandLineOptions Rg "[]=" `shouldBe` ["[]=", ".", "-F", "-c", "-j", "1"] + commandLineOptions Rg "window.globalOverride" `shouldBe` ["window.globalOverride", ".", "-F", "-c", "-j", "1"] + + it "uses regular expression match with surrounding non-word matches for accuracy" $ do + commandLineOptions Ag "awesome_method" `shouldBe` ["(\\W|^)awesome_method(\\W|$)", ".", "-c", "--ackmate", "--ignore-dir", "tmp/unused"] + commandLineOptions Rg "awesome_method" `shouldBe` ["(\\W|^)awesome_method(\\W|$)", ".", "-c", "-j", "1"] describe "parseSearchResult" $ do - it "parses normal results from `ag` to a TermMatch" $ - parseSearchResult (OriginalTerm "method_name") ":app/models/foo.rb:123" `shouldBe` (Just $ TermMatch "method_name" "app/models/foo.rb" Nothing 123) + it "parses normal results from `ag` to a TermMatch" $ do + parseSearchResult Ag (OriginalTerm "method_name") ":app/models/foo.rb:123" `shouldBe` (Just $ TermMatch "method_name" "app/models/foo.rb" Nothing 123) + parseSearchResult Rg (OriginalTerm "method_name") "app/models/foo.rb:123" `shouldBe` (Just $ TermMatch "method_name" "app/models/foo.rb" Nothing 123) - it "returns Nothing when it cannot parse" $ - parseSearchResult (OriginalTerm "method_name") "" `shouldBe` Nothing + it "returns Nothing when it cannot parse" $ do + parseSearchResult Ag (OriginalTerm "method_name") "" `shouldBe` Nothing + parseSearchResult Rg (OriginalTerm "method_name") "" `shouldBe` Nothing