git.iko.soy/rowtype-yoga/purescript-docs-search
on master help sign in
added: CLI interface; added: search REPL (#5)

* added: CLI interface added: search REPL * added: shebang to the executable file * added: automatic deployments * better heuristics for package name detection * added: previews

commit4ce20e3251f8f069a3990b93a1aff9438496abbf authorVladimir Kalnitsky <klntsky@gmail.com> dateSun 21 Jul 2019, 17:29 (7 years ago) parente69e813
25 files changed+1030−236
.gitignore+1 .travis.yml+12−5 README.md+23−10 package.json+43 packages.dhall+22 preview.png spago.dhall+2 src/Docs/Search/App/SearchResults.purs+33−68 src/Docs/Search/Config.purs+7−6 src/Docs/Search/Declarations.purs+11−3 src/Docs/Search/DocsJson.purs+2−5 src/Docs/Search/Engine.purs+59 src/Docs/Search/Extra.js+9 src/Docs/Search/Extra.purs+21−1 src/Docs/Search/Index.purs+1 src/Docs/Search/IndexBuilder.js+7 src/Docs/Search/IndexBuilder.purs+124−63 src/Docs/Search/Interactive.js+5 src/Docs/Search/Interactive.purs+258 src/Docs/Search/Main.purs+103 src/Docs/Search/Terminal.purs+23 src/Docs/Search/TypeIndex.purs+6−6 src/Docs/Search/TypePrinter.purs+156 src/Docs/Search/TypeQuery.purs+100−57 test/Test/IndexBuilder.purs+2−12
.gitignore+1
@@ -8,3 +8,4 @@
88/.purs*
99/.psa*
1010/.spago/
11+/dist/
.travis.yml+12−5
@@ -19,24 +19,31 @@ install:
1919- tar -xvf $HOME/purescript.tar.gz -C $HOME/
2020- chmod a+x $HOME/purescript
2121- npm install -g spago
22+- npm install
2223- spago install
2324
2425script:
2526- spago build
2627- spago test
2728- spago docs
28−- spago bundle-app -m Docs.Search.App --to docs-search-app.js
29−- spago bundle-app -m Docs.Search.IndexBuilder --to index-builder.js
30−- node index-builder.js
29+- npm run build
30+- node dist/main.js build-index
3131
3232deploy:
3333 - provider: releases
3434 api_key: $API_KEY
3535 file:
36− - docs-search-app.js
37− - index-builder.js
36+ - dist/docs-search-app.js
37+ - dist/main.js
3838 skip_cleanup: true
3939 on:
4040 tags: true
4141 script:
4242 - echo 'done'
43+ - provider: npm
44+ api_key: $NPM_API_KEY
45+ email: klntsky@gmail.com
46+ skip_cleanup: true
47+ on:
48+ tags: true
49+ branch: master
README.md+23−10
@@ -4,21 +4,34 @@
44
55An app that adds search capabilities to generated documentation for purescript code.
66
7−The goal is to replicate all functionality of pursuit, including querying by type.
7+It supports nearly-all functionality of [Pursuit](https://github.com/purescript/pursuit), including querying by type.
88
9−See [#89](https://github.com/spacchetti/spago/issues/89).
9+## Installing
1010
11−To see it in action, run the following:
11+Run `npm install purescript-docs-search`.
1212
13−```
14−spago build
15−spago docs
16−spago bundle-app -m Docs.Search.App --to generated-docs/docs-search-app.js
17−spago run -m Docs.Search.IndexBuilder
18−```
13+## Usage
1914
20−## UI
15+There are two usage scenarios:
16+
17+### Patching static documentation
18+
19+Use `purescript-docs-search build-index` command to patch HTML files located in `generated-docs/html`. You then will be able to search for declarations or types:
20+
21+![Preview](preview.png)
2122
2223The user interface of the app is optimised for keyboard-only use.
2324
2425**S** hotkey can be used to focus on the search field, **Escape** can be used to leave it. Pressing **Escape** twice will close the search results listing.
26+
27+### Using the CLI
28+
29+Running `purescript-docs-search` within a project directory will open an interactive command-line session.
30+
31+Note that unlike in Pursuit, most relevant results will appear last.
32+
33+A quick demo:
34+
35+[![asciicast](https://asciinema.org/a/Hexie5JoWjlAqLqv2IgafIdb9.svg)](https://asciinema.org/a/Hexie5JoWjlAqLqv2IgafIdb9)
36+
37+You may notice that the CLI offers slightly better results than the web interface. This is a performance tradeoff.
package.jsonnew file+43
@@ -0,0 +1,43 @@
1+{
2+ "name": "purescript-docs-search",
3+ "version": "0.0.1",
4+ "description": "Search frontend for the documentation generated by the PureScript compiler.",
5+ "main": "dist/main.js",
6+ "directories": {
7+ "test": "test"
8+ },
9+ "bin": {
10+ "purescript-docs-search": "dist/main.js"
11+ },
12+ "files": [
13+ "dist/main.js",
14+ "dist/docs-search-app.js",
15+ "README.md"
16+ ],
17+ "scripts": {
18+ "test": "spago test",
19+ "bundle-app": "spago bundle-app -m Docs.Search.App --to dist/docs-search-app.js",
20+ "bundle-main": "spago bundle-app -m Docs.Search.Main --to dist/main.js && browserify --no-builtins --no-commondir --no-detect-globals --node dist/main.js --outfile dist/main-bundled.js && echo \"#!/usr/bin/env node\" > dist/main.js && cat dist/main-bundled.js >> dist/main.js && rm dist/main-bundled.js",
21+ "build": "npm run bundle-app && npm run bundle-main",
22+ "clean": "rm -rf dist"
23+ },
24+ "repository": {
25+ "type": "git",
26+ "url": "git+https://github.com/spacchetti/purescript-docs-search.git"
27+ },
28+ "keywords": [
29+ "purescript"
30+ ],
31+ "author": "Kalnitsky Vladimir <klntsky@gmail.com>",
32+ "license": "BSD-3-Clause",
33+ "bugs": {
34+ "url": "https://github.com/spacchetti/purescript-docs-search/issues"
35+ },
36+ "homepage": "https://github.com/spacchetti/purescript-docs-search#readme",
37+ "dependencies": {},
38+ "devDependencies": {
39+ "browserify": "^16.3.0",
40+ "glob": "^7.1.4",
41+ "spago": "^0.8.5"
42+ }
43+}
packages.dhall+22
@@ -47,6 +47,28 @@ let additions =
4747 [ "css", "halogen" ]
4848 "https://github.com/slamdata/purescript-halogen-css.git"
4949 "v8.0.0"
50+ , optparse =
51+ mkPackage
52+ [ "prelude"
53+ , "effect"
54+ , "exitcodes"
55+ , "strings"
56+ , "ordered-collections"
57+ , "arrays"
58+ , "console"
59+ , "memoize"
60+ , "transformers"
61+ , "exists"
62+ , "node-process"
63+ , "free"
64+ ]
65+ "https://github.com/f-o-a-m/purescript-optparse.git"
66+ "v3.0.1"
67+ , exitcodes =
68+ mkPackage
69+ [ "enums" ]
70+ "https://github.com/Risto-Stevcev/purescript-exitcodes.git"
71+ "v4.0.0"
5072 }
5173
5274in upstream ⫽ overrides ⫽ additions
preview.pngnew file

Binary file not shown.

spago.dhall+2
@@ -21,6 +21,8 @@
2121 , "node-fs"
2222 , "node-fs-aff"
2323 , "node-process"
24+ , "node-readline"
25+ , "optparse"
2426 , "profunctor"
2527 , "search-trie"
2628 , "string-parsers"
src/Docs/Search/App/SearchResults.purs+33−68
@@ -7,24 +7,19 @@ import Docs.Search.Config (config)
77import Docs.Search.Declarations (DeclLevel(..), declLevelToHashAnchor)
88import Docs.Search.DocsJson (DataDeclType(..))
99import Docs.Search.Extra ((>#>))
10−import Docs.Search.Index (Index)
11−import Docs.Search.Index as Index
12−import Docs.Search.SearchResult (ResultInfo(..), SearchResult, typeOf)
10+import Docs.Search.SearchResult (ResultInfo(..), SearchResult)
1311import Docs.Search.TypeDecoder (Constraint(..), FunDep(..), FunDeps(..), Kind(..), QualifiedName(..), Type(..), TypeArgument(..), joinForAlls, joinRows)
14−import Docs.Search.TypeIndex (TypeIndex)
15−import Docs.Search.TypeIndex as TypeIndex
16−import Docs.Search.TypeQuery (TypeQuery(..), parseTypeQuery, penalty)
12+import Docs.Search.Engine as SearchEngine
13+import Docs.Search.Engine (ResultsType(..))
1714
1815import CSS (textWhitespace, whitespacePreWrap)
1916import Data.Array ((!!))
2017import Data.Array as Array
21−import Data.Either (hush)
2218import Data.List as List
23−import Data.Maybe (Maybe(..), isJust, maybe)
19+import Data.Maybe (Maybe(..), isJust)
2420import Data.Newtype (unwrap, wrap)
25−import Data.String (length) as String
2621import Data.String.CodeUnits (stripSuffix) as String
27−import Data.String.Common (toLower, trim) as String
22+import Data.String.Common (null, trim) as String
2823import Data.String.Pattern (Pattern(..)) as String
2924import Effect.Aff (Aff)
3025import Halogen as H
@@ -38,15 +33,11 @@ import Web.HTML as HTML
3833import Web.HTML.Location as Location
3934import Web.HTML.Window as Window
4035
41−data Mode = Off | Loading | Active | InputTooShort
36+data Mode = Off | Loading | Active
4237
4338derive instance eqMode :: Eq Mode
4439
45−-- | Is it a search by type or by name?
46−data ResultsType = TypeResults TypeQuery | DeclResults
47−
48−type State = { index :: Index
49− , typeIndex :: TypeIndex
40+type State = { searchEngineState :: SearchEngine.State
5041 , results :: Array SearchResult
5142 , resultsType :: ResultsType
5243 , input :: String
@@ -68,8 +59,7 @@ mkComponent
6859 -> H.Component HH.HTML Query i o Aff
6960mkComponent contents =
7061 H.mkComponent
71− { initialState: const { index: mempty
72− , typeIndex: mempty
62+ { initialState: const { searchEngineState: mempty
7363 , results: []
7464 , resultsType: DeclResults
7565 , input: ""
@@ -99,36 +89,20 @@ handleQuery (MessageFromSearchField (InputUpdated input_) next) = do
9989
10090 state <- H.modify (_ { input = input })
10191
102− if String.length input < 2
92+ if String.null input
10393 then do
104− if input == ""
105− then do
10694 H.modify_ (_ { mode = Off })
10795 showPageContents
108− else do
109− H.modify_ (_ { mode = InputTooShort })
110− hidePageContents
11196 else do
11297 H.modify_ (_ { mode = Loading, resultsCount = config.resultsCount })
11398
11499 void $ H.fork do
115− let resultsType =
116− maybe DeclResults TypeResults (hush (parseTypeQuery state.input)
117− >>= isValuableTypeQuery)
118−
119− case resultsType of
120−
121− DeclResults -> do
122− { index, results } <- H.liftAff $ Index.query state.index (String.toLower state.input)
123− H.modify_ (_ { results = results
124− , mode = Active
125− , index = index })
126−
127− TypeResults query -> do
128− { index, results } <- H.liftAff $ TypeIndex.query state.typeIndex query
129− H.modify_ (_ { results = sortByDistance query results
130− , mode = Active
131− , typeIndex = index })
100+ { searchEngineState, results, resultsType } <- H.liftAff $
101+ SearchEngine.query state.searchEngineState state.input
102+ H.modify_ (_ { results = results
103+ , mode = Active
104+ , searchEngineState = searchEngineState
105+ , resultsType = resultsType })
132106
133107 hidePageContents
134108
@@ -181,12 +155,6 @@ render { mode: Off } = HH.div_ []
181155render { mode: Loading } =
182156 renderContainer $
183157 [ HH.h1_ [ HH.text "Loading..." ] ]
184−render { mode: InputTooShort } =
185− renderContainer $
186− [ HH.h1_ [ HH.text "Error" ] ] <>
187− [ HH.div [ HP.classes [ wrap "result", wrap "result--empty" ] ]
188− [ HH.text "Search query is too short." ]
189− ]
190158render state@{ mode: Active, results: [] } =
191159 renderContainer $
192160
@@ -299,11 +267,7 @@ renderResultType
299267renderResultType result =
300268 case result.info of
301269 ValueResult { type: ty } ->
302− wrapSignature [ HH.a [ makeHref ValueLevel false result.moduleName result.name
303− , HE.onClick $ const $ Just $ SearchResultClicked result.moduleName ]
304− [ HH.text result.name ]
305− , HH.text " :: "
306− , renderType ty ]
270+ wrapSignature $ renderValueSignature result ty
307271
308272 TypeClassResult info ->
309273 wrapSignature $ renderTypeClassSignature info result
@@ -321,6 +285,21 @@ renderResultType result =
321285 wrapSignature signature =
322286 [ HH.pre [ HP.class_ (wrap "result__signature") ] [ HH.code_ signature ] ]
323287
288+renderValueSignature
289+ :: forall a rest
290+ . { moduleName :: String
291+ , name :: String
292+ | rest
293+ }
294+ -> Type
295+ -> Array (HH.HTML a Action)
296+renderValueSignature result ty =
297+ [ HH.a [ makeHref ValueLevel false result.moduleName result.name
298+ , HE.onClick $ const $ Just $ SearchResultClicked result.moduleName ]
299+ [ HH.text result.name ]
300+ , HH.text " :: "
301+ , renderType ty ]
302+
324303renderTypeClassSignature
325304 :: forall a rest
326305 . { fundeps :: FunDeps
@@ -582,8 +561,8 @@ renderQualifiedName isInfix level (QualifiedName { moduleName, name })
582561
583562renderKind
584563 :: forall a
585− . Kind ->
586− HH.HTML a Action
564+ . Kind
565+ -> HH.HTML a Action
587566renderKind = case _ of
588567 Row k1 -> HH.span_ [ HH.text "# ", renderKind k1 ]
589568 FunKind k1 k2 -> HH.span_ [ renderKind k1, syntax " -> ", renderKind k2 ]
@@ -616,17 +595,3 @@ syntax str = HH.span [ HP.class_ (wrap "syntax") ] [ HH.text str ]
616595
617596space :: forall a b. HH.HTML a b
618597space = HH.text " "
619−
620−isValuableTypeQuery :: TypeQuery -> Maybe TypeQuery
621−isValuableTypeQuery (QVar _) = Nothing
622−isValuableTypeQuery (QConst _) = Nothing
623−isValuableTypeQuery query = Just query
624−
625−sortByDistance :: TypeQuery -> Array SearchResult -> Array SearchResult
626−sortByDistance typeQuery results =
627− _.result <$> Array.sortBy comparePenalties resultsWithPenalties
628− where
629− comparePenalties r1 r2 = compare r1.penalty r2.penalty
630− resultsWithPenalties = results <#>
631− \result -> { penalty: typeOf (unwrap result).info <#> penalty typeQuery
632− , result }
src/Docs/Search/Config.purs+7−6
@@ -17,19 +17,20 @@ config =
1717 , numberOfIndexParts: 50
1818 -- ^ In how many parts the index should be splitted?
1919 , mkIndexPartPath:
20− \(partId :: Int) -> "generated-docs/index/declarations/" <> show partId <> ".js"
20+ \(partId :: Int) -> "/index/declarations/" <> show partId <> ".js"
2121 , mkIndexPartLoadPath:
2222 \(partId :: Int) -> "../index/declarations/" <> show partId <> ".js"
2323 , resultsCount: 25
2424 -- ^ How many results to show by default?
25− , penalties: { typeVars: 6
25+ , penalties: { typeVars: 2
2626 , match: 2
2727 , matchConstraint: 1
28− , instantiate: 1
29− , generalize: 4
30− , rowsMismatch: 6
31− , mismatch: 10
28+ , instantiate: 2
29+ , generalize: 2
30+ , rowsMismatch: 3
3231 , missingConstraint: 1
3332 , excessiveConstraint: 1
3433 }
34+ -- ^ Penalties used to determine how "far" a type query is from a given type.
35+ -- See Docs.Search.TypeQuery
3536 }
src/Docs/Search/Declarations.purs+11−3
@@ -8,6 +8,7 @@ import Docs.Search.TypeDecoder (Constraint(..), QualifiedName(..), Type(..), joi
88
99import Control.Alt ((<|>))
1010import Data.Array ((!!))
11+import Data.Array as Array
1112import Data.Foldable (foldr)
1213import Data.List (List, (:))
1314import Data.List as List
@@ -15,8 +16,8 @@ import Data.Maybe (Maybe(..), fromMaybe)
1516import Data.Newtype (class Newtype, unwrap, wrap)
1617import Data.Search.Trie (Trie, alter)
1718import Data.String.CodeUnits (stripPrefix, stripSuffix, toCharArray)
18−import Data.String.Common (toLower)
1919import Data.String.Common (split) as String
20+import Data.String.Common (toLower)
2021import Data.String.Pattern (Pattern(..))
2122
2223type ModuleName = String
@@ -178,11 +179,18 @@ extractPackageName name =
178179 let chunks = String.split (Pattern "/") name in
179180 fromMaybe "<unknown>" $
180181 chunks !! 0 >>= \dir ->
181− -- TODO: is it safe to assume that directory name is ".spago"?
182182 if dir == ".spago" then
183183 chunks !! 1
184184 else
185− Just "<local package>"
185+ let
186+ bowerComponentsIndex =
187+ Array.findIndex (_ == "bower_components") chunks
188+ in
189+ case bowerComponentsIndex of
190+ Just n ->
191+ chunks !! (n + 1)
192+ Nothing ->
193+ Just "<local package>"
186194
187195resultsForChildDeclaration
188196 :: PackageName
src/Docs/Search/DocsJson.purs+2−5
@@ -2,10 +2,9 @@ module Docs.Search.DocsJson where
22
33import Prelude
44
5−import Docs.Search.TypeDecoder
5+import Docs.Search.TypeDecoder (Constraint, FunDeps, Kind, Type, TypeArgument)
66
7−import Control.Promise (Promise, toAffE)
8−import Data.Argonaut.Core (Json, fromString, stringify, toString)
7+import Data.Argonaut.Core (fromString, stringify, toString)
98import Data.Argonaut.Decode (class DecodeJson, decodeJson, (.:), (.:?))
109import Data.Argonaut.Encode (class EncodeJson, encodeJson)
1110import Data.Either (Either(..))
@@ -13,8 +12,6 @@ import Data.Generic.Rep (class Generic)
1312import Data.Generic.Rep.Show (genericShow)
1413import Data.Maybe (Maybe(..))
1514import Data.Newtype (class Newtype, unwrap)
16−import Effect (Effect)
17−import Effect.Aff (Aff)
1815
1916newtype DocsJson
2017 = DocsJson { name :: String
src/Docs/Search/Engine.pursnew file+59
@@ -0,0 +1,59 @@
1+module Docs.Search.Engine where
2+
3+import Prelude
4+
5+import Docs.Search.TypeQuery (TypeQuery(..), parseTypeQuery, penalty)
6+import Docs.Search.SearchResult (SearchResult, typeOf)
7+import Docs.Search.Index as Index
8+import Docs.Search.Index (Index)
9+import Docs.Search.TypeIndex as TypeIndex
10+import Docs.Search.TypeIndex (TypeIndex)
11+
12+import Data.Array as Array
13+import Data.Either (hush)
14+import Data.Maybe (Maybe(..))
15+import Data.Newtype (unwrap)
16+import Data.String.Common as String
17+import Effect.Aff (Aff)
18+
19+data ResultsType = TypeResults TypeQuery | DeclResults
20+
21+type State = { index :: Index
22+ , typeIndex :: TypeIndex
23+ }
24+
25+query
26+ :: State
27+ -> String
28+ -> Aff { searchEngineState :: State
29+ , results :: Array SearchResult
30+ , resultsType :: ResultsType
31+ }
32+query { index, typeIndex } input =
33+ case hush (parseTypeQuery input) >>= isValuableTypeQuery of
34+ Nothing -> do
35+ response <- Index.query index (String.toLower input)
36+ pure { searchEngineState: { index: response.index, typeIndex }
37+ , results: response.results
38+ , resultsType: DeclResults }
39+
40+ Just typeQuery -> do
41+ response <- TypeIndex.query typeIndex typeQuery
42+ pure { searchEngineState: { index, typeIndex: response.typeIndex }
43+ , results: sortByDistance typeQuery response.results
44+ , resultsType: TypeResults typeQuery }
45+
46+isValuableTypeQuery :: TypeQuery -> Maybe TypeQuery
47+isValuableTypeQuery (QVar _) = Nothing
48+isValuableTypeQuery (QConst _) = Nothing
49+isValuableTypeQuery other = Just other
50+
51+sortByDistance :: TypeQuery -> Array SearchResult -> Array SearchResult
52+sortByDistance typeQuery results =
53+ _.result <$> Array.sortBy comparePenalties resultsWithPenalties
54+ where
55+ comparePenalties r1 r2 = compare r1.penalty r2.penalty
56+ resultsWithPenalties =
57+ results <#>
58+ \result -> { penalty: typeOf (unwrap result).info <#> penalty typeQuery
59+ , result }
src/Docs/Search/Extra.jsnew file+9
@@ -0,0 +1,9 @@
1+/* global exports require */
2+
3+var glob = require('glob');
4+
5+exports.glob = function (pattern) {
6+ return function () {
7+ return glob.sync(pattern);
8+ };
9+};
src/Docs/Search/Extra.purs+21−1
@@ -2,8 +2,12 @@ module Docs.Search.Extra where
22
33import Prelude
44
5−import Data.Foldable (class Foldable, foldMap)
5+import Data.Foldable (class Foldable, foldMap, foldl)
6+import Data.List.NonEmpty (NonEmptyList, cons', uncons)
67import Data.Maybe (Maybe(..))
8+import Effect (Effect)
9+import Data.List as List
10+import Data.List ((:))
711
812whenJust :: forall a m. Monad m => Maybe a -> (a -> m Unit) -> m Unit
913whenJust (Just a) f = f a
@@ -13,3 +17,19 @@ foldMapFlipped :: forall a m f. Foldable f => Monoid m => f a -> (a -> m) -> m
1317foldMapFlipped = flip foldMap
1418
1519infixr 7 foldMapFlipped as >#>
20+
21+foreign import glob :: String -> Effect (Array String)
22+
23+foldl1 :: forall a. (a -> a -> a) -> NonEmptyList a -> a
24+foldl1 f as =
25+ case uncons as of
26+ { head, tail } -> foldl f head tail
27+
28+foldr1 :: forall a. (a -> a -> a) -> NonEmptyList a -> a
29+foldr1 f = go List.Nil
30+ where
31+ go acc x = case uncons x of
32+ { head, tail } -> case List.uncons tail of
33+ Nothing -> List.foldl (flip f) head acc
34+ Just { head: head1, tail: tail1 } ->
35+ go (head : acc) (cons' head1 tail1)
src/Docs/Search/Index.purs+1
@@ -98,6 +98,7 @@ getPartId (a : _) =
9898 Char.toCharCode a `mod` config.numberOfIndexParts
9999getPartId _ = 0
100100
101+-- | Load a part of the index by injecting a <script> tag into the DOM.
101102foreign import loadIndex_
102103 :: Int
103104 -> String
src/Docs/Search/IndexBuilder.jsnew file+7
@@ -0,0 +1,7 @@
1+/* global __dirname require exports */
2+
3+var path = require('path');
4+
5+exports.getDirname = function () {
6+ return __dirname;
7+};
src/Docs/Search/IndexBuilder.purs+124−63
@@ -5,7 +5,7 @@ import Prelude
55import Docs.Search.Config (config)
66import Docs.Search.Declarations (Declarations(..), mkDeclarations)
77import Docs.Search.DocsJson (DocsJson)
8−import Docs.Search.Extra ((>#>))
8+import Docs.Search.Extra ((>#>), glob)
99import Docs.Search.Index (getPartId)
1010import Docs.Search.SearchResult (SearchResult)
1111import Docs.Search.TypeIndex (TypeIndex, mkTypeIndex)
@@ -35,55 +35,83 @@ import Effect.Aff (Aff, launchAff_, parallel, sequential)
3535import Effect.Class (liftEffect)
3636import Effect.Console (log)
3737import Node.Encoding (Encoding(UTF8))
38−import Node.FS.Aff (exists, mkdir, readTextFile, readdir, stat, writeTextFile)
38+import Node.FS.Aff (exists, mkdir, readFile, readTextFile, readdir, stat, writeFile, writeTextFile)
3939import Node.FS.Stats (isDirectory, isFile)
4040import Node.Process as Process
4141
42−main :: Effect Unit
43−main = launchAff_ mainAff
42+type Config = { docsFiles :: Array String
43+ , generatedDocs :: String
44+ }
4445
45−mainAff :: Aff Unit
46−mainAff = do
47− checkDirectories
46+run :: Config -> Effect Unit
47+run = launchAff_ <<< run'
4848
49− docsJsons <- collectDocsJsons config.outputDirectory
49+run' :: Config -> Aff Unit
50+run' cfg = do
5051
51− liftEffect $ log $
52− "Found " <> show (Array.length docsJsons) <> " modules."
52+ checkDirectories cfg
53+
54+ liftEffect do
55+ log "Building the search index..."
56+
57+ docsJsons <- decodeDocsJsons cfg
58+
59+ liftEffect do
60+ log $ "Found " <> show (Array.length docsJsons) <> " modules."
5361
5462 let index = mkDeclarations docsJsons
5563 typeIndex = mkTypeIndex index
5664
57− createDirectories
65+ createDirectories cfg
5866
5967 void $ sequential do
60− ignore <$> parallel (writeIndex index)
61− <*> parallel (writeTypeIndex typeIndex)
62− <*> parallel patchDocs
63−
64− liftEffect $ log $
65− "Loaded " <>
66− show (Trie.size $ unwrap index) <>
67− " definitions and " <>
68− show (List.length $ join $ map snd $ Trie.entriesUnordered (unwrap index)) <>
69− " type definitions"
70− where ignore _ _ _ = unit
68+ ignore <$> parallel (writeIndex cfg index)
69+ <*> parallel (writeTypeIndex cfg typeIndex)
70+ <*> parallel (patchDocs cfg)
71+ <*> parallel (copyAppFile cfg)
72+
73+ liftEffect do
74+ log $
75+ "Added " <>
76+ show (Trie.size $ unwrap index) <>
77+ " definitions and " <>
78+ show (List.length $ join $ map snd $ Trie.entriesUnordered (unwrap index)) <>
79+ " type definitions to the search index."
80+
81+ where ignore _ _ _ _ = unit
7182
7283-- | Exit early if something is missing.
73−checkDirectories :: Aff Unit
74−checkDirectories = do
75− for_ config.requiredDirectories \dir -> do
76− whenM (not <$> directoryExists dir) $
77− liftEffect $ logAndExit "Generate the documentation first!"
84+checkDirectories :: Config -> Aff Unit
85+checkDirectories cfg = do
7886
79−-- | Read and decode all `docs.json` files in the `outputDir`.
80−collectDocsJsons :: String -> Aff (Array DocsJson)
81−collectDocsJsons outputDir = do
82− paths <- readdir outputDir
87+ let dirs = [ cfg.generatedDocs
88+ , cfg.generatedDocs <> "/html"
89+ ]
8390
84− Array.catMaybes <$> for paths \moduleName -> do
85− let jsonFile = "output/" <> moduleName <> "/docs.json"
91+ for_ dirs \dir -> do
92+ whenM (not <$> directoryExists dir) $
93+ liftEffect do
94+ logAndExit "Build the documentation first!"
95+
96+-- | Read and decode given `docs.json` files.
97+decodeDocsJsons
98+ :: forall rest
99+ . { docsFiles :: Array String | rest }
100+ -> Aff (Array DocsJson)
101+decodeDocsJsons cfg = do
102+
103+ paths <- Array.concat <$> for cfg.docsFiles \str -> do
104+ liftEffect $ glob str
105+
106+ when (Array.null paths) do
107+ liftEffect do
108+ logAndExit $
109+ "The following globs do not match any files: " <> showGlobs cfg.docsFiles <>
110+ ".\nBuild the documentation first!"
111+
112+ docsJsons <- Array.catMaybes <$> for paths \jsonFile -> do
86113 doesExist <- fileExists jsonFile
114+
87115 if doesExist then do
88116
89117 contents <- readTextFile UTF8 jsonFile
@@ -92,20 +120,27 @@ collectDocsJsons outputDir = do
92120 case eiResult of
93121 Left error -> do
94122 liftEffect $ log $
95− "\"docs.json\" decoding failed failed for module " <> moduleName <> ": " <> show error
123+ "\"docs.json\" decoding failed failed for " <> jsonFile <> ": " <> show error
96124 pure Nothing
97125 Right result -> pure result
98126
99127 else do
100128 liftEffect $ do
101129 log $
102− "Couldn't find docs.json for " <> moduleName
130+ "File does not exist: " <> jsonFile
103131 pure Nothing
104132
105−writeTypeIndex :: TypeIndex -> Aff Unit
106−writeTypeIndex typeIndex =
133+ when (Array.null docsJsons) do
134+ liftEffect $ logAndExit $
135+ "Couldn't decode any of the files matched by the following globs: " <> showGlobs cfg.docsFiles
136+
137+ pure docsJsons
138+
139+-- | Write type index parts to files.
140+writeTypeIndex :: Config -> TypeIndex -> Aff Unit
141+writeTypeIndex { generatedDocs } typeIndex =
107142 for_ entries \(Tuple typeShape results) -> do
108− writeTextFile UTF8 ("generated-docs/index/types/" <> typeShape <> ".js")
143+ writeTextFile UTF8 (generatedDocs <> "/index/types/" <> typeShape <> ".js")
109144 (mkHeader typeShape <> stringify (encodeJson results))
110145 where
111146 mkHeader typeShape =
@@ -119,11 +154,7 @@ getIndex :: Declarations -> Map Int (Array (Tuple String (Array SearchResult)))
119154getIndex (Declarations trie) =
120155 Array.foldr insert mempty parts
121156 where
122− prefixes :: Array (List Char)
123− prefixes =
124− Set.toUnfoldable $
125− List.foldr (\entry -> Set.insert (List.take 2 $ fst entry)) mempty $
126− Trie.entriesUnordered trie
157+ insert part = Map.insertWith append (getPartId part.prefix) part.results
127158
128159 parts
129160 :: Array { prefix :: List Char
@@ -131,24 +162,35 @@ getIndex (Declarations trie) =
131162 }
132163 parts = prefixes <#> \prefix ->
133164 let results =
134− Array.fromFoldable $
135− Trie.query prefix trie <#>
136− \(Tuple path value) ->
137− Tuple (path >#> String.singleton) (Array.fromFoldable value)
165+ Array.fromFoldable $ toTuple <$>
166+ if List.length prefix == 2 then
167+ Trie.query prefix trie
168+ else
169+ -- Entries with path lengths > 1 have been added already.
170+ List.filter (\(Tuple path value) -> List.length path == 1) (
171+ Trie.query prefix trie
172+ )
138173 in
139174 { prefix, results }
140175
141− insert part = Map.insertWith append (getPartId part.prefix) part.results
176+ toTuple (Tuple path value) =
177+ Tuple (path >#> String.singleton) (Array.fromFoldable value)
142178
143−writeIndex :: Declarations -> Aff Unit
144−writeIndex = getIndex >>> \resultsMap -> do
179+ prefixes :: Array (List Char)
180+ prefixes =
181+ Set.toUnfoldable $
182+ List.foldr (\path -> Set.insert (List.take 2 path)) mempty $
183+ fst <$> Trie.entriesUnordered trie
184+
185+writeIndex :: Config -> Declarations -> Aff Unit
186+writeIndex { generatedDocs } = getIndex >>> \resultsMap -> do
145187 for_ (Map.toUnfoldableUnordered resultsMap :: Array _)
146188 \(Tuple indexPartId results) -> do
147189 let header =
148190 "// This file was generated by purescript-docs-search.\n" <>
149191 "window.DocsSearchIndex[\"" <> show indexPartId <> "\"] = "
150192
151− writeTextFile UTF8 (config.mkIndexPartPath indexPartId) $
193+ writeTextFile UTF8 (generatedDocs <> config.mkIndexPartPath indexPartId) $
152194 header <> stringify (encodeJson results)
153195
154196patchHTML :: String -> Tuple Boolean String
@@ -166,9 +208,9 @@ patchHTML html =
166208 then Tuple true $ String.replace pattern (Replacement patch) html
167209 else Tuple false html
168210
169−patchDocs :: Aff Unit
170−patchDocs = do
171− let dirname = "generated-docs/"
211+patchDocs :: Config -> Aff Unit
212+patchDocs cfg = do
213+ let dirname = cfg.generatedDocs
172214
173215 files <- readdir (dirname <> "html")
174216
@@ -182,14 +224,13 @@ patchDocs = do
182224 writeTextFile UTF8 path patchedContents
183225 _ -> pure unit
184226
185−createDirectories :: Aff Unit
186−createDirectories = do
187− let generatedDocsDir = "generated-docs/"
188− indexDir = "generated-docs/index"
189− declIndexDir = "generated-docs/index/declarations"
190− typeIndexDir = "generated-docs/index/types"
227+createDirectories :: Config -> Aff Unit
228+createDirectories { generatedDocs } = do
229+ let indexDir = generatedDocs <> "/index"
230+ declIndexDir = generatedDocs <> "/index/declarations"
231+ typeIndexDir = generatedDocs <> "/index/types"
191232
192− whenM (not <$> directoryExists generatedDocsDir) $ liftEffect do
233+ whenM (not <$> directoryExists generatedDocs) $ liftEffect do
193234 logAndExit "Generate the documentation first!"
194235
195236 whenM (not <$> directoryExists indexDir) do
@@ -201,6 +242,20 @@ createDirectories = do
201242 whenM (not <$> directoryExists typeIndexDir) do
202243 mkdir typeIndexDir
203244
245+-- | Copy the client-side application, responsible for handling user input and rendering
246+-- | the results, to the destination path.
247+copyAppFile :: Config -> Aff Unit
248+copyAppFile { generatedDocs } = do
249+ appDir <- liftEffect getDirname
250+ let appFile = appDir <> "/docs-search-app.js"
251+ whenM (not <$> fileExists appFile) do
252+ liftEffect do
253+ logAndExit $
254+ "Client-side app was not found at " <> appFile <> ".\n" <>
255+ "Check your installation."
256+ buffer <- readFile appFile
257+ writeFile (generatedDocs <> "/docs-search-app.js") buffer
258+
204259directoryExists :: String -> Aff Boolean
205260directoryExists path = do
206261 doesExist <- exists path
@@ -216,6 +271,12 @@ fileExists path = do
216271 true -> isFile <$> stat path
217272
218273logAndExit :: forall a. String -> Effect a
219−logAndExit err = do
220− log err
274+logAndExit message = do
275+ log message
221276 Process.exit 1
277+
278+showGlobs :: Array String -> String
279+showGlobs = Array.intercalate ", "
280+
281+-- | Get __dirname.
282+foreign import getDirname :: Effect String
src/Docs/Search/Interactive.jsnew file+5
@@ -0,0 +1,5 @@
1+/* global exports */
2+
3+exports.consoleClear = function () {
4+ console.clear();
5+};
src/Docs/Search/Interactive.pursnew file+258
@@ -0,0 +1,258 @@
1+module Docs.Search.Interactive where
2+
3+import Prelude
4+
5+import Docs.Search.Declarations (Declarations, mkDeclarations)
6+import Docs.Search.DocsJson (DataDeclType(..))
7+import Docs.Search.Engine (isValuableTypeQuery)
8+import Docs.Search.Engine as SearchEngine
9+import Docs.Search.IndexBuilder as IndexBuilder
10+import Docs.Search.SearchResult (ResultInfo(..), SearchResult(..))
11+import Docs.Search.Terminal (bold, cyan, green, yellow)
12+import Docs.Search.TypeDecoder (Constraint, FunDeps, Kind, QualifiedName, Type, TypeArgument)
13+import Docs.Search.TypeIndex (mkTypeIndex)
14+import Docs.Search.TypePrinter (keyword, showConstraint, showFunDeps, showKind, showType, showTypeArgument, space, syntax)
15+import Docs.Search.TypeQuery (parseTypeQuery)
16+
17+import Data.Array as Array
18+import Data.Either (hush)
19+import Data.List as List
20+import Data.Map as Map
21+import Data.Maybe (Maybe(..), fromMaybe)
22+import Data.Newtype (unwrap, wrap)
23+import Data.Search.Trie as Trie
24+import Data.String (length) as String
25+import Data.String.CodeUnits (fromCharArray, toCharArray) as String
26+import Data.String.Common (split, toLower, trim) as String
27+import Data.Tuple (snd, fst)
28+import Effect (Effect)
29+import Effect.Aff (launchAff_)
30+import Effect.Class (liftEffect)
31+import Effect.Console (log)
32+import Node.ReadLine (createConsoleInterface, question)
33+
34+type Config = { docsFiles :: Array String }
35+
36+run :: Config -> Effect Unit
37+run cfg = launchAff_ $ do
38+
39+ liftEffect do
40+ log "Loading search index..."
41+
42+ docsJsons <- IndexBuilder.decodeDocsJsons cfg
43+
44+ let index = mkDeclarations docsJsons
45+ typeIndex = Array.concat $ Array.fromFoldable (
46+ Map.values (unwrap (mkTypeIndex index)) <#> fromMaybe []
47+ ) :: Array SearchResult
48+
49+ liftEffect do
50+ log $
51+ "Loaded " <>
52+ show (Trie.size $ unwrap index) <>
53+ " definitions and " <>
54+ show (List.length $ join $ map snd $ Trie.entriesUnordered (unwrap index)) <>
55+ " type definitions"
56+
57+ liftEffect do
58+ let
59+ call handler interface = do
60+ question "> " (handler interface) interface
61+
62+ inputHandler interface input = do
63+ let results =
64+ case hush (parseTypeQuery input) >>= isValuableTypeQuery of
65+ Nothing ->
66+ Array.fromFoldable $
67+ List.concat $
68+ Trie.queryValues (List.fromFoldable $
69+ String.toCharArray $
70+ String.toLower $
71+ input) (unwrap index)
72+
73+ Just typeQuery ->
74+ Array.take 100 $
75+ SearchEngine.sortByDistance typeQuery typeIndex
76+
77+ let total = Array.length results
78+
79+ consoleClear
80+
81+ if total > 0 then do
82+ log $
83+ Array.intercalate "\n\n\n" $
84+ showResult <$> Array.reverse results
85+ else do
86+ log $
87+ "Your search for " <> bold input <> " did not yield any results."
88+ call inputHandler interface
89+
90+ interface <- do
91+ interface <- createConsoleInterface (mkCompleter index)
92+ pure interface
93+
94+ call inputHandler interface
95+
96+mkCompleter
97+ :: Declarations
98+ -> String
99+ -> Effect { completions :: Array String
100+ , matched :: String }
101+mkCompleter index input = do
102+ let path = List.fromFoldable $ String.toCharArray input
103+ let paths =
104+ Array.fromFoldable $
105+ (String.fromCharArray <<< Array.fromFoldable) <$>
106+ (fst <$> Trie.query path (unwrap index))
107+
108+ pure { completions: paths
109+ , matched: input }
110+
111+showResult :: SearchResult -> String
112+showResult result@(SearchResult { name, comments, moduleName, packageName, info }) =
113+ showSignature result <> "\n" <>
114+
115+ (fromMaybe "\n" $
116+ comments <#> \comment ->
117+ "\n" <> leftShift 3 (String.trim comment) <> "\n\n") <>
118+
119+ bold (cyan (rightPad 40 packageName)) <> space <> bold (green moduleName)
120+
121+showSignature :: SearchResult -> String
122+showSignature result@(SearchResult { name, info }) =
123+ case info of
124+ ValueResult { type: ty } ->
125+ yellow name <> syntax " :: " <> showType ty
126+
127+ TypeClassResult info' ->
128+ showTypeClassSignature info' (unwrap result)
129+
130+ TypeClassMemberResult info' ->
131+ showTypeClassMemberSignature info' (unwrap result)
132+
133+ DataResult info' ->
134+ showDataSignature info' (unwrap result)
135+
136+ TypeSynonymResult info' ->
137+ showTypeSynonymSignature info' (unwrap result)
138+
139+ ExternDataResult info' ->
140+ showExternDataSignature info' (unwrap result)
141+
142+ ValueAliasResult ->
143+ yellow ("(" <> name <> ")")
144+
145+ _ -> yellow name
146+
147+showTypeClassSignature
148+ :: forall rest
149+ . { fundeps :: FunDeps
150+ , arguments :: Array TypeArgument
151+ , superclasses :: Array Constraint
152+ }
153+ -> { name :: String, moduleName :: String | rest }
154+ -> String
155+showTypeClassSignature { fundeps, arguments, superclasses } { name, moduleName } =
156+
157+ keyword "class" <>
158+ ( if Array.null superclasses
159+ then
160+ ""
161+ else
162+ syntax " (" <> (
163+ Array.intercalate (syntax ", " ) (
164+ superclasses <#> showConstraint
165+ )
166+ ) <>
167+ syntax ")" <>
168+ space <>
169+ syntax "<="
170+ ) <>
171+ space <>
172+ yellow name <>
173+ space <> (
174+ Array.intercalate space $
175+ arguments <#> showTypeArgument
176+ ) <> (
177+ showFunDeps fundeps
178+ )
179+
180+showTypeClassMemberSignature
181+ :: forall rest
182+ . { type :: Type
183+ , typeClass :: QualifiedName
184+ , typeClassArguments :: Array String
185+ }
186+ -> { name :: String | rest }
187+ -> String
188+showTypeClassMemberSignature { type: ty, typeClass, typeClassArguments } result =
189+ yellow result.name <>
190+ syntax " :: " <>
191+ showType ty
192+
193+showDataSignature
194+ :: forall rest
195+ . { typeArguments :: Array TypeArgument
196+ , dataDeclType :: DataDeclType }
197+ -> { name :: String | rest }
198+ -> String
199+showDataSignature { typeArguments, dataDeclType } { name } =
200+ ( keyword
201+ case dataDeclType of
202+ NewtypeDataDecl -> "newtype"
203+ DataDataDecl -> "data"
204+ ) <>
205+ space <>
206+ yellow name <>
207+ space <> (
208+ Array.intercalate space $
209+ typeArguments <#> showTypeArgument
210+ )
211+
212+showTypeSynonymSignature
213+ :: forall rest
214+ . { type :: Type
215+ , arguments :: Array TypeArgument
216+ }
217+ -> { name :: String | rest }
218+ -> String
219+showTypeSynonymSignature { type: ty, arguments } { name } =
220+ keyword "type" <>
221+ space <>
222+ yellow name <>
223+ space <> (
224+ Array.intercalate space $
225+ arguments <#> showTypeArgument
226+ ) <>
227+ space <>
228+ syntax "=" <>
229+ space <>
230+ showType ty
231+
232+showExternDataSignature
233+ :: forall rest
234+ . { kind :: Kind }
235+ -> { name :: String | rest }
236+ -> String
237+showExternDataSignature { kind } { name } =
238+ keyword "foreign data" <>
239+ space <>
240+ yellow name <>
241+ space <>
242+ syntax " :: " <>
243+ showKind kind
244+
245+leftShift :: Int -> String -> String
246+leftShift shift str =
247+ Array.intercalate "\n" $
248+ leftPad shift <$>
249+ String.trim <$>
250+ String.split (wrap "\n") str
251+
252+leftPad :: Int -> String -> String
253+leftPad w str = Array.fold (Array.replicate w " ") <> str
254+
255+rightPad :: Int -> String -> String
256+rightPad w str = str <> Array.fold (Array.replicate (w - String.length str) " ")
257+
258+foreign import consoleClear :: Effect Unit
src/Docs/Search/Main.pursnew file+103
@@ -0,0 +1,103 @@
1+module Docs.Search.Main where
2+
3+import Prelude
4+
5+import Docs.Search.IndexBuilder as IndexBuilder
6+import Docs.Search.Interactive as Interactive
7+
8+import Data.Generic.Rep (class Generic)
9+import Data.Generic.Rep.Show (genericShow)
10+import Data.List as List
11+import Data.List.NonEmpty as NonEmpty
12+import Data.Maybe (Maybe, fromMaybe, optional)
13+import Data.Unfoldable (class Unfoldable)
14+import Effect (Effect)
15+import Options.Applicative (Parser, command, execParser, fullDesc, helper, info, long, metavar, progDesc, strOption, subparser, value, (<**>))
16+import Options.Applicative as CA
17+
18+main :: Effect Unit
19+main = do
20+
21+ args <- getArgs
22+ let defaultCommands = Search { docsFiles: defaultDocsFiles }
23+
24+ case fromMaybe defaultCommands args of
25+ BuildIndex cfg -> IndexBuilder.run cfg
26+ Search cfg -> Interactive.run cfg
27+
28+getArgs :: Effect (Maybe Commands)
29+getArgs = execParser opts
30+ where
31+ opts =
32+ info (commands <**> helper)
33+ ( fullDesc
34+ <> progDesc "Search frontend for the documentation generated by the PureScript compiler."
35+ )
36+
37+data Commands
38+ = BuildIndex { docsFiles :: Array String
39+ , generatedDocs :: String
40+ }
41+ | Search { docsFiles :: Array String }
42+
43+derive instance genericCommands :: Generic Commands _
44+
45+instance showCommands :: Show Commands where
46+ show = genericShow
47+
48+commands :: Parser (Maybe Commands)
49+commands = optional $ subparser
50+ ( command "build-index"
51+ ( info buildIndex
52+ ( progDesc "Build the index used to search for definitions and patch the generated docs so that they include a search field."
53+ )
54+ )
55+ <> command "search"
56+ ( info startInteractive
57+ ( progDesc "Run the search engine."
58+ )
59+ )
60+ )
61+
62+buildIndex :: Parser Commands
63+buildIndex = ado
64+
65+ docsFiles <- fromMaybe defaultDocsFiles <$>
66+ optional (
67+ some ( strOption
68+ ( long "docs-files"
69+ <> metavar "GLOB"
70+ )
71+ )
72+ )
73+
74+ generatedDocs <- strOption
75+ ( long "generated-docs"
76+ <> metavar "DIR"
77+ <> value "./generated-docs/"
78+ )
79+
80+ in BuildIndex { docsFiles, generatedDocs }
81+
82+startInteractive :: Parser Commands
83+startInteractive = ado
84+
85+ docsFiles <- fromMaybe defaultDocsFiles <$>
86+ optional (
87+ some ( strOption
88+ ( long "docs-files"
89+ <> metavar "GLOB"
90+ )
91+ )
92+ )
93+
94+ in Search { docsFiles }
95+
96+defaultDocsFiles :: Array String
97+defaultDocsFiles = [ "output/**/docs.json" ]
98+
99+many :: forall a f. Unfoldable f => Parser a -> Parser (f a)
100+many x = CA.many x <#> List.toUnfoldable
101+
102+some :: forall a f. Unfoldable f => Parser a -> Parser (f a)
103+some x = CA.some x <#> NonEmpty.toUnfoldable
src/Docs/Search/Terminal.pursnew file+23
@@ -0,0 +1,23 @@
1+module Docs.Search.Terminal where
2+
3+import Prelude
4+
5+bold :: String -> String
6+bold str =
7+ "\x1b[1m" <> str <> "\x1b[0m"
8+
9+yellow :: String -> String
10+yellow str =
11+ "\x1b[33m" <> str <> "\x1b[0m"
12+
13+grey :: String -> String
14+grey str =
15+ "\x1b[37m" <> str <> "\x1b[0m"
16+
17+cyan :: String -> String
18+cyan str =
19+ "\x1b[36m" <> str <> "\x1b[0m"
20+
21+green :: String -> String
22+green str =
23+ "\x1b[32m" <> str <> "\x1b[0m"
src/Docs/Search/TypeIndex.purs+6−6
@@ -69,23 +69,23 @@ mkTypeIndex (Declarations trie) = TypeIndex $ map (Array.fromFoldable >>> Just)
6969lookup
7070 :: String
7171 -> TypeIndex
72− -> Aff { index :: TypeIndex, results :: Array SearchResult }
73−lookup key index@(TypeIndex map) =
72+ -> Aff { typeIndex :: TypeIndex, results :: Array SearchResult }
73+lookup key typeIndex@(TypeIndex map) =
7474 case Map.lookup key map of
75− Just results -> pure { index, results: Array.fold results }
75+ Just results -> pure { typeIndex, results: Array.fold results }
7676 Nothing -> do
7777 eiJson <- try (toAffE (lookup_ key $ config.mkShapeScriptPath key))
7878 pure $ fromMaybe'
79− (\_ -> { index: insert key Nothing index, results: [] })
79+ (\_ -> { typeIndex: insert key Nothing typeIndex, results: [] })
8080 do
8181 json <- hush eiJson
8282 results <- hush (decodeJson json)
83− pure { index: insert key (Just results) index, results }
83+ pure { typeIndex: insert key (Just results) typeIndex, results }
8484
8585query
8686 :: TypeIndex
8787 -> TypeQuery
88− -> Aff { index :: TypeIndex, results :: Array SearchResult }
88+ -> Aff { typeIndex :: TypeIndex, results :: Array SearchResult }
8989query typeIndex typeQuery = do
9090 res <- lookup (stringifyShape $ shapeOfTypeQuery typeQuery) typeIndex
9191 pure $ res { results = sortByRelevance typeQuery res.results }
src/Docs/Search/TypePrinter.pursnew file+156
@@ -0,0 +1,156 @@
1+module Docs.Search.TypePrinter where
2+
3+import Prelude
4+
5+import Docs.Search.Terminal (cyan)
6+import Docs.Search.TypeDecoder (Constraint(..), FunDep(..), FunDeps(..), Kind(..), QualifiedName(..), Type(..), TypeArgument(..), joinForAlls, joinRows)
7+
8+import Data.Maybe (Maybe(..))
9+import Data.Array as Array
10+import Data.List as List
11+
12+showType :: Type -> String
13+showType = case _ of
14+ TypeVar str -> str
15+ TypeLevelString str -> "\"" <> str <> "\"" -- TODO: add escaping
16+ TypeWildcard -> "_"
17+ TypeConstructor qname -> showQualifiedName qname
18+ TypeOp qname -> showQualifiedName qname
19+
20+ TypeApp (TypeApp (TypeConstructor
21+ (QualifiedName { moduleName: [ "Prim" ]
22+ , name: "Function" })) t1) t2 ->
23+ showType t1 <> syntax " -> " <> showType t2
24+
25+ TypeApp (TypeConstructor (QualifiedName { moduleName: [ "Prim" ]
26+ , name: "Record" }))
27+ row ->
28+ showRow false row
29+
30+ TypeApp t1 t2 ->
31+ showType t1 <> " " <> showType t2
32+
33+ ty@(ForAll _ _ _) ->
34+ showForAll ty
35+
36+ ConstrainedType cnstr ty ->
37+ showConstraint cnstr <>
38+ syntax " => " <>
39+ showType ty
40+
41+ ty@REmpty -> showRow true ty
42+ ty@(RCons _ _ _) -> showRow true ty
43+
44+ BinaryNoParensType op t1 t2 ->
45+ showType t1 <>
46+ space <>
47+ showType op <>
48+ space <>
49+ showType t2
50+
51+ ParensInType ty ->
52+ "(" <>
53+ showType ty <>
54+ ")"
55+
56+showTypeArgument :: TypeArgument -> String
57+showTypeArgument (TypeArgument { name, mbKind }) =
58+ case mbKind of
59+ Nothing ->
60+ name
61+ Just kind ->
62+ "(" <>
63+ name <>
64+ " :: " <>
65+ showKind kind <>
66+ ")"
67+
68+showFunDeps :: FunDeps -> String
69+showFunDeps (FunDeps []) = ""
70+showFunDeps (FunDeps deps) =
71+ append (syntax " | ") $
72+ Array.intercalate (syntax ", ") $
73+ deps <#> renderFunDep
74+ where
75+ renderFunDep (FunDep { lhs, rhs }) =
76+ Array.intercalate space lhs <>
77+ syntax " -> " <>
78+ Array.intercalate space rhs
79+
80+showQualifiedName
81+ :: QualifiedName
82+ -> String
83+showQualifiedName (QualifiedName { name })
84+ = name
85+
86+showRow
87+ :: Boolean
88+ -> Type
89+ -> String
90+showRow asRow =
91+ joinRows >>> \ { rows, ty } ->
92+ if List.null rows
93+ then
94+ if asRow then "()" else "{}"
95+ else
96+ opening <>
97+ ( Array.intercalate ", " $ Array.fromFoldable $ rows <#>
98+ \entry ->
99+ entry.row <> syntax " :: " <> showType entry.ty
100+ ) <>
101+
102+ case ty of
103+ Just ty' -> " | " <> showType ty' <> closing
104+ Nothing -> closing
105+
106+ where
107+ opening = if asRow then "(" else "{ "
108+ closing = if asRow then ")" else " }"
109+
110+showForAll
111+ :: Type
112+ -> String
113+showForAll ty =
114+ keyword "forall" <>
115+
116+ ( Array.fold $ foralls.binders <#>
117+ \ { var, mbKind } ->
118+ case mbKind of
119+ Nothing -> " " <> var
120+ Just kind ->
121+ " (" <> var <> " "
122+ <> syntax "::"
123+ <> space
124+ <> showKind kind
125+ <> ")"
126+ ) <>
127+
128+ syntax ". " <>
129+ showType foralls.ty
130+
131+ where
132+ foralls = joinForAlls ty
133+
134+showKind
135+ :: Kind
136+ -> String
137+showKind = case _ of
138+ Row k1 -> "# " <> showKind k1
139+ FunKind k1 k2 -> showKind k1 <> syntax " -> " <> showKind k2
140+ NamedKind qname -> showQualifiedName qname
141+
142+showConstraint
143+ :: Constraint
144+ -> String
145+showConstraint (Constraint { constraintClass, constraintArgs }) =
146+ showQualifiedName constraintClass <> space <>
147+ Array.intercalate space (constraintArgs <#> showType)
148+
149+syntax :: String -> String
150+syntax = cyan
151+
152+space :: String
153+space = " "
154+
155+keyword :: String -> String
156+keyword = cyan
src/Docs/Search/TypeQuery.purs+100−57
@@ -1,3 +1,4 @@
1+-- | `TypeQuery` is a representation of a user-provided type.
12module Docs.Search.TypeQuery
23 ( TypeQuery(..)
34 , Substitution(..)
@@ -11,24 +12,21 @@ where
1112
1213import Prelude
1314
14−import Docs.Search.Config
15−import Docs.Search.DocsJson
16−import Docs.Search.TypeDecoder
15+import Docs.Search.Config (config)
16+import Docs.Search.Extra (foldl1, foldr1)
17+import Docs.Search.TypeDecoder (QualifiedName(..), Type(..), joinConstraints, joinRows)
1718
1819import Control.Alt ((<|>))
1920import Data.Array as Array
2021import Data.Either (Either)
21−import Data.Foldable (foldl)
2222import Data.Generic.Rep (class Generic)
2323import Data.Generic.Rep.Show (genericShow)
2424import Data.List (List(..), many, some, (:))
2525import Data.List as List
26−import Data.List.NonEmpty (NonEmptyList, cons', uncons)
26+import Data.List.NonEmpty (NonEmptyList)
2727import Data.List.NonEmpty as NonEmptyList
2828import Data.Map (Map)
2929import Data.Map as Map
30−import Data.Maybe (Maybe(..))
31−import Data.Newtype (unwrap)
3230import Data.Ord (abs)
3331import Data.Set (Set)
3432import Data.Set as Set
@@ -93,25 +91,12 @@ typeQueryParser = fix \typeQuery ->
9391
9492 constrained =
9593 QConstraint <$> (upperCaseIdent <* skipSpaces) <*>
96− (sepEndBy ((QVar <$> ident) <|> parens) (many space) <* string "=>" <* skipSpaces) <*>
94+ (sepEndBy ((QVar <$> ident) <|> parens)
95+ (many space) <* string "=>" <* skipSpaces) <*>
9796 typeQuery
9897 in
9998 try constrained <|> funs
10099
101−foldl1 :: forall a. (a -> a -> a) -> NonEmptyList a -> a
102−foldl1 f as =
103− case uncons as of
104− { head, tail } -> foldl f head tail
105−
106−foldr1 :: forall a. (a -> a -> a) -> NonEmptyList a -> a
107−foldr1 f = go List.Nil
108− where
109− go acc x = case uncons x of
110− { head, tail } -> case List.uncons tail of
111− Nothing -> List.foldl (flip f) head acc
112− Just { head: head1, tail: tail1 } ->
113− go (head : acc) (cons' head1 tail1)
114−
115100any :: Parser TypeQuery
116101any = do
117102 QVar <$> lowerCaseIdent
@@ -188,7 +173,12 @@ data Substitution
188173 | MissingConstraint
189174 | ExcessiveConstraint
190175 | RowsMismatch Int Int
191− | Mismatch
176+ | Mismatch TypeQuery Type
177+ -- ^ Type and type query significantly differ.
178+ | TypeMismatch Type
179+ -- ^ A query of size 1 corresponds to some type.
180+ | QueryMismatch TypeQuery
181+ -- ^ A type of size 1 corresponds to some query.
192182
193183derive instance genericSubstitution :: Generic Substitution _
194184
@@ -204,14 +194,24 @@ unify query type_ = go Nil (List.singleton { q: query, t: type_ })
204194 go acc ({ q, t } : rest)
205195
206196 -- * ForAll
207− go acc ({ q: (QForAll queryBinders q), t:type_1@(ForAll _ _ _) } : rest) =
208− let { binders, ty } = joinForAlls type_1 in
209− go acc ({ q, t: ty } : rest)
210197 go acc ({ q, t: ForAll _ t _ } : rest) =
211198 go acc ({ q, t } : rest)
212− go acc ({ q: (QForAll queryBinders q), t } : rest) =
199+ go acc ({ q: (QForAll _ q), t } : rest) =
213200 go acc ({ q, t } : rest)
214201
202+ -- * Constraints
203+ go acc ({ q: q@(QConstraint _ _ _)
204+ , t: t@(ConstrainedType _ _) } : rest) =
205+ let qcs = Set.fromFoldable (joinQueryConstraints q).constraints
206+ tcs = Set.fromFoldable (joinConstraints t).constraints
207+ in
208+ -- TODO: use edit distance instead
209+ go (MatchConstraints qcs tcs : acc) rest
210+ go acc ({ q: QConstraint _ _ q, t } : rest) =
211+ go (ExcessiveConstraint : acc) ({ q, t } : rest)
212+ go acc ({ q, t: ConstrainedType _ t } : rest) =
213+ go (MissingConstraint : acc) ({ q, t } : rest)
214+
215215 -- * Type variables
216216 go acc ({ q: QVar q, t: TypeVar v } : rest) =
217217 go (Substitute q v : acc) rest
@@ -224,15 +224,15 @@ unify query type_ = go Nil (List.singleton { q: query, t: type_ })
224224 go acc ({ q: QConst qname, t: TypeConstructor (QualifiedName { name }) } : rest) =
225225 go (Match qname name : acc) rest
226226 go acc ({ q: QConst qname, t } : rest) =
227− go (Mismatch : acc) rest
227+ go (TypeMismatch t : acc) rest
228228 go acc ({ q, t: TypeConstructor (QualifiedName { name }) } : rest) =
229− go (Mismatch : acc) rest
229+ go (QueryMismatch q : acc) rest
230230
231231 -- type operators can't appear in type queries: this is always a mismatch
232232 go acc ({ q, t: TypeOp (QualifiedName { name }) } : rest) =
233− go (Mismatch : acc) rest
234− go acc ({ q, t: BinaryNoParensType _ _ _ } : rest) =
235− go (Mismatch : acc) rest
233+ go (QueryMismatch q : acc) rest
234+ go acc ({ q, t: t@(BinaryNoParensType _ _ _) } : rest) =
235+ go (Mismatch q t : acc) rest
236236
237237 -- * Functions
238238 go acc ({ q: QFun q1 q2
@@ -240,21 +240,8 @@ unify query type_ = go Nil (List.singleton { q: query, t: type_ })
240240 (QualifiedName { moduleName: [ "Prim" ]
241241 , name: "Function" })) t1) t2 } : rest) =
242242 go acc ({ q: q1, t: t1 } : { q: q2, t: t2 } : rest)
243− go acc ({ q: QFun q1 q2, t } : rest) =
244− go (Mismatch : acc) rest
245−
246− -- * Constraints
247− go acc ({ q: q@(QConstraint _ _ _)
248− , t: t@(ConstrainedType _ _) } : rest) =
249− let qcs = Set.fromFoldable (joinQueryConstraints q).constraints
250− tcs = Set.fromFoldable (joinConstraints t).constraints
251− in
252− -- TODO: use edit distance instead
253− go (MatchConstraints qcs tcs : acc) rest
254− go acc ({ q: QConstraint _ _ q, t } : rest) =
255− go (ExcessiveConstraint : acc) ({ q, t } : rest)
256− go acc ({ q, t: ConstrainedType _ t } : rest) =
257− go (MissingConstraint : acc) ({ q, t } : rest)
243+ go acc ({ q: q@(QFun q1 q2), t } : rest) =
244+ go (Mismatch q t : acc) rest
258245
259246 -- * Rows
260247 go acc ({ q: QApp (QConst "Record") (QRow qRows)
@@ -278,24 +265,24 @@ unify query type_ = go Nil (List.singleton { q: query, t: type_ })
278265 else
279266 go (RowsMismatch qRowsLength rowsLength : acc) rest
280267
281− go acc ({ q: QRow _ } : rest) =
282− go (Mismatch : acc) rest
268+ go acc ({ q: q@(QRow _), t } : rest) =
269+ go (Mismatch q t : acc) rest
283270
284271 -- * Type application
285272 go acc ({ q: QApp q1 q2, t: TypeApp t1 t2 } : rest) =
286273 go acc ({ q: q1, t: t1 } : { q: q2, t: t2 } : rest)
287274
288275 go acc ({ q, t: TypeLevelString _ } : rest) =
289− go (Mismatch : acc) rest
276+ go (QueryMismatch q : acc) rest
290277
291278 go acc ({ q, t: TypeWildcard } : rest) =
292− go (Mismatch : acc) rest
279+ go (QueryMismatch q : acc) rest
293280
294− go acc ({ q, t: RCons _ _ _ } : rest) =
295− go (Mismatch : acc) rest
281+ go acc ({ q, t: t@(RCons _ _ _) } : rest) =
282+ go (Mismatch q t : acc) rest
296283
297284 go acc ({ q, t: REmpty } : rest) =
298− go (Mismatch : acc) rest
285+ go (QueryMismatch q : acc) rest
299286
300287penalty :: TypeQuery -> Type -> Int
301288penalty typeQuery ty =
@@ -332,7 +319,6 @@ namesPenalty :: List Substitution -> Int
332319namesPenalty = go 0
333320 where
334321 go p Nil = p
335− go p (Mismatch : rest) = go (p + config.penalties.mismatch) rest
336322 go p (Match a b : rest)
337323 | a == b = go p rest
338324 | otherwise = go (p + config.penalties.match) rest
@@ -348,10 +334,15 @@ mismatchPenalty :: List Substitution -> Int
348334mismatchPenalty = go 0
349335 where
350336 go n Nil = n
351− go n (Instantiate _ _ : rest) = go (n + config.penalties.instantiate) rest
352− go n (Generalize _ _ : rest) = go (n + config.penalties.generalize) rest
337+ go n (Instantiate q t : rest) = go (n + typeSize t *
338+ config.penalties.instantiate) rest
339+ go n (Generalize q t : rest) = go (n + typeQuerySize q *
340+ config.penalties.generalize) rest
353341 go n (ExcessiveConstraint : rest) = go (n + config.penalties.excessiveConstraint) rest
354342 go n (MissingConstraint : rest) = go (n + config.penalties.missingConstraint) rest
343+ go n (Mismatch q t : rest) = go (n + typeQuerySize q + typeSize t) rest
344+ go n (TypeMismatch t : rest) = go (n + typeSize t) rest
345+ go n (QueryMismatch q : rest) = go (n + typeQuerySize q) rest
355346 go n (_ : rest) = go n rest
356347
357348-- | Only returns a list of type class names (lists of arguments are omitted).
@@ -362,3 +353,55 @@ joinQueryConstraints = go Nil
362353 go acc (QConstraint name _ query) =
363354 go (name : acc) query
364355 go acc ty = { constraints: List.sort acc, ty }
356+
357+typeQuerySize :: TypeQuery -> Int
358+typeQuerySize = go 0 <<< List.singleton
359+ where
360+ go n Nil = n
361+ go n (QVar _ : rest) =
362+ go (n + 1) rest
363+ go n (QConst _ : rest) =
364+ go (n + 1) rest
365+ go n (QFun q1 q2 : rest) =
366+ go (n + 1) (q1 : q2 : rest)
367+ go n (QApp q1 q2 : rest) =
368+ go (n + 1) (q1 : q2 : rest)
369+ go n (QForAll _ q : rest) =
370+ go (n + 1) (q : rest)
371+ go n (QConstraint _ _ q : rest) =
372+ go (n + 1) (q : rest)
373+ go n (QRow qs : rest) =
374+ go n ((qs <#> snd) <> rest)
375+
376+typeSize :: Type -> Int
377+typeSize = go 0 <<< List.singleton
378+ where
379+ go n Nil = n
380+ go n (TypeVar _ : rest) =
381+ go (n + 1) rest
382+ go n (TypeLevelString _ : rest) =
383+ go (n + 1) rest
384+ go n (TypeWildcard : rest) =
385+ go (n + 1) rest
386+ go n (TypeConstructor _ : rest) =
387+ go (n + 1) rest
388+ go n (TypeOp _ : rest) =
389+ go (n + 1) rest
390+ go n (TypeApp (TypeApp (TypeConstructor
391+ (QualifiedName { moduleName: [ "Prim" ]
392+ , name: "Function" })) t1) t2 : rest) =
393+ go (n + 1) (t1 : t2 : rest)
394+ go n (TypeApp q1 q2 : rest) =
395+ go (n + 1) (q1 : q2 : rest)
396+ go n (ForAll _ t _ : rest) =
397+ go (n + 1) (t : rest)
398+ go n (ConstrainedType _ t : rest) =
399+ go (n + 1) (t : rest)
400+ go n (RCons _ t1 t2 : rest) =
401+ go (n + 1) (t1 : t2 : rest)
402+ go n (REmpty : rest) =
403+ go (n + 1) rest
404+ go n (BinaryNoParensType op t1 t2 : rest) =
405+ go (n + 1) (t1 : t2 : rest)
406+ go n (ParensInType t : rest) =
407+ go n (t : rest)
test/Test/IndexBuilder.purs+2−12
@@ -2,19 +2,9 @@ module Test.IndexBuilder where
22
33import Prelude
44
5−import Test.Extra
5+import Docs.Search.IndexBuilder (patchHTML)
66
7−import Docs.Search.IndexBuilder
8−
9−import Data.Either (Either(..))
10−import Data.Foldable (class Foldable)
11−import Data.List (List(..), (:))
12−import Data.List as List
13−import Data.List.NonEmpty (NonEmptyList)
14−import Data.List.NonEmpty as NonEmptyList
15−import Data.Set as Set
16−import Data.Tuple (Tuple(..), snd)
17−import Effect.Aff (Aff)
7+import Data.Tuple (snd)
188import Test.Unit (TestSuite, suite, test)
199import Test.Unit.Assert as Assert
2010