git.iko.soy/rowtype-yoga/purescript-docs-search
on master help sign in
improvements
commitc2c729c45f13c5402e8d6e1e6ce8c8f4a5345053 authorklntsky <klntsky@gmail.com> dateMon 24 Jun 2019, 21:23 (7 years ago) parent7bcebed
5 files changed+222−97
src/Spago/Search/App.purs+31−27 src/Spago/Search/App/SearchResults.purs+107−44 src/Spago/Search/Declarations.purs+18−8 src/Spago/Search/Index.purs+44−9 src/Spago/Search/IndexBuilder.purs+22−9
src/Spago/Search/App.purs+31−27
@@ -1,33 +1,27 @@
11module Spago.Search.App where
22
3−import Effect.Console
43import Prelude
54
6−import Control.Promise (Promise, toAffE)
5+import Spago.Search.App.SearchField as SearchField
6+import Spago.Search.App.SearchResults as SearchResults
7+import Spago.Search.Extra (whenJust)
8+
9+import Control.Coroutine as Coroutine
710import Data.Maybe (Maybe(..))
811import Data.Newtype (wrap)
912import Effect (Effect)
10−import Effect.Aff (Aff, launchAff_)
11−import Effect.Class (liftEffect)
13+import Halogen as H
1214import Halogen.Aff as HA
1315import Halogen.VDom.Driver (runUI)
14−import Spago.Search.App.SearchField as SearchField
15−import Spago.Search.App.SearchResults as SearchResults
16−import Spago.Search.Declarations (Declarations)
17−import Spago.Search.Index (mkSearchIndex)
18−import Spago.Search.Extra
19−import Web.DOM as DOM
2016import Web.DOM.Document as Document
2117import Web.DOM.Element as Element
22−import Web.DOM.Text as Text
2318import Web.DOM.Node as Node
2419import Web.DOM.ParentNode as ParentNode
20+import Web.DOM.Text as Text
2521import Web.HTML as HTML
2622import Web.HTML.HTMLDocument as HTMLDocument
27−import Web.HTML.HTMLElement as HTMLElement
23+import Web.HTML.HTMLElement (fromElement)
2824import Web.HTML.Window as Window
29−import Control.Coroutine as Coroutine
30−import Halogen as H
3125
3226main :: Effect Unit
3327main = do
@@ -35,12 +29,12 @@ main = do
3529 doc <- HTMLDocument.toDocument <$> Window.document win
3630
3731 insertStyle doc
38− mbContainer <- findContainer doc
32+ mbContainers <- getContainers doc
3933
40− whenJust mbContainer \container -> do
34+ whenJust mbContainers \ { searchField, searchResults, pageContents } -> do
4135 HA.runHalogenAff do
42− sfio <- runUI SearchField.component unit container
43− srio <- runUI SearchResults.component unit container
36+ sfio <- runUI SearchField.component unit searchField
37+ srio <- runUI (SearchResults.mkComponent pageContents) unit searchResults
4438 sfio.subscribe $
4539 Coroutine.consumer \message ->
4640 srio.query $ H.tell $ SearchResults.SearchFieldMessage message
@@ -62,13 +56,23 @@ insertStyle doc = do
6256 void $ Node.appendChild (Text.toNode contents) (Element.toNode style)
6357 void $ Node.appendChild (Element.toNode style) (Element.toNode head)
6458
65−findContainer :: Document.Document -> Effect (Maybe HTML.HTMLElement)
66−findContainer doc = do
59+getContainers :: Document.Document -> Effect (Maybe { searchField :: HTML.HTMLElement
60+ , searchResults :: HTML.HTMLElement
61+ , pageContents :: Element.Element })
62+getContainers doc = do
63+ let docPN = Document.toParentNode doc
6764 mbBanner <-
68− ParentNode.querySelector (wrap ".top-banner > .container") (Document.toParentNode doc)
69− case mbBanner of
70− Nothing -> pure Nothing
71− Just banner -> do
72− container <- Document.createElement "div" doc
73− void $ Node.appendChild (Element.toNode container) (Element.toNode banner)
74− pure $ HTMLElement.fromElement container
65+ ParentNode.querySelector (wrap ".top-banner > .container") docPN
66+ mbEverything <-
67+ ParentNode.querySelector (wrap ".everything-except-footer") docPN
68+ mbContainer <-
69+ ParentNode.querySelector (wrap ".everything-except-footer > .container") docPN
70+ case mbBanner, mbEverything, mbContainer of
71+ Just banner, Just everything, Just pageContents -> do
72+ search <- Document.createElement "div" doc
73+ void $ Node.appendChild (Element.toNode search) (Element.toNode banner)
74+ pure $ fromElement search >>= \searchField ->
75+ fromElement everything >>= \searchResults ->
76+ pure { searchField, searchResults, pageContents }
77+ _, _, _ ->
78+ pure Nothing
src/Spago/Search/App/SearchResults.purs+107−44
@@ -2,66 +2,71 @@ module Spago.Search.App.SearchResults where
22
33import Prelude
44
5−import Spago.Search.App.SearchField (Message(..))
6−import Spago.Search.Declarations
7−import Spago.Search.Index
8−import Spago.Search.Extra
5+import Spago.Search.Declarations (loadDeclarations)
6+import Spago.Search.Extra (whenJust)
7+import Spago.Search.Index (SearchIndex, SearchResult, mkSearchIndex)
98
10−import CSS hiding (render,map)
119import Data.Array as Array
10+import Data.Either (Either(..))
1211import Data.List as List
13−import Data.Maybe
14−import Data.Newtype
12+import Data.Maybe (Maybe(..), isNothing)
13+import Data.Newtype (unwrap, wrap)
1514import Data.Search.Trie as Trie
16−import Data.String (null) as String
15+import Data.String (length) as String
1716import Data.String.CodeUnits (toCharArray) as String
1817import Data.String.Common (toLower) as String
19−import Effect.Aff
20−import Effect.Console
18+import Effect.Aff (Aff)
19+import Effect.Console (error)
2120import Halogen as H
2221import Halogen.HTML as HH
23−import Halogen.HTML.CSS as HS
24−import Halogen.HTML.Events as HE
2522import Halogen.HTML.Properties as HP
23+import Spago.Search.App.SearchField (Message(..))
24+import Web.DOM.Element (Element)
25+import Web.DOM.Element as Element
2626
27−type State = { shown :: Boolean
28− , mbIndex :: Maybe SearchIndex
27+type State = { mbIndex :: Maybe SearchIndex
2928 , results :: Array SearchResult
3029 , input :: String
30+ , contents :: Element
3131 }
3232
3333data Query a
3434 = SearchFieldMessage Message a
3535
36−component :: forall i o. H.Component HH.HTML Query i o Aff
37−component =
36+mkComponent :: forall i o. Element -> H.Component HH.HTML Query i o Aff
37+mkComponent contents =
3838 H.mkComponent
3939 { initialState:
40− const { mbIndex: Nothing, results: [], shown: false, input: "" }
40+ const { mbIndex: Nothing, results: [], input: "", contents }
4141 , render
4242 , eval: H.mkEval $ H.defaultEval { handleQuery = handleQuery }
4343 }
4444
4545handleQuery :: forall o a. Query a -> H.HalogenM State Unit () o Aff (Maybe a)
4646handleQuery (SearchFieldMessage Focused next) = do
47− H.modify_ (_ { shown = true })
4847 state <- H.get
4948 when (isNothing state.mbIndex) do
50− declarations <- H.liftAff $ loadDeclarations "../spago-search-index.js"
51− H.modify_ (_ { mbIndex = Just $ mkSearchIndex declarations })
49+ eiDeclarations <- H.liftAff $ loadDeclarations "../spago-search-index.js"
50+ case eiDeclarations of
51+ Left err -> do
52+ H.liftEffect do
53+ error $ "spago-search: couldn't load search index: " <> err
54+ Right declarations -> do
55+ H.modify_ (_ { mbIndex = Just $ mkSearchIndex declarations })
5256 pure Nothing
5357handleQuery (SearchFieldMessage LostFocus next) = do
5458 state <- H.get
5559 when (Array.null state.results) do
56− H.modify_ (_ { shown = false })
60+ showPageContents
5761 pure Nothing
5862handleQuery (SearchFieldMessage (InputUpdated input) next) = do
5963 H.modify_ (_ { input = input })
60−
61− if String.null input
64+ if String.length input < 2
6265 then do
66+ showPageContents
6367 H.modify_ (_ { results = [] })
6468 else do
69+ hidePageContents
6570 state <- H.get
6671 whenJust (unwrap <$> state.mbIndex) \index -> do
6772 let path = List.fromFoldable $
@@ -76,33 +81,91 @@ handleQuery (SearchFieldMessage (InputUpdated input) next) = do
7681
7782 pure Nothing
7883
84+showPageContents :: forall o. H.HalogenM State Unit () o Aff Unit
85+showPageContents = do
86+ state <- H.get
87+ H.liftEffect do
88+ Element.removeAttribute "style" state.contents
89+
90+hidePageContents :: forall o. H.HalogenM State Unit () o Aff Unit
91+hidePageContents = do
92+ state <- H.get
93+ H.liftEffect do
94+ Element.setAttribute "style" "display: none" state.contents
95+
7996render :: forall m. State -> H.ComponentHTML Unit () m
80−render { mbIndex: Nothing, shown: true } =
81− renderSummary "Loading index..."
82−render { mbIndex: Nothing, shown: false } =
97+render { mbIndex: Nothing } =
98+ HH.div_ []
99+render { input: "" } =
83100 HH.div_ []
84−render state
85− | not state.shown || String.null state.input = HH.div_ []
86− | otherwise =
87− HH.div
88− [ HS.style do
89− float floatLeft
101+render state =
102+ HH.div [ HP.classes [ wrap "container"
103+ , wrap "clearfix"
104+ ]
105+ ]
106+ if Array.null state.results
107+ then
108+ [ renderSummary "No search results" ]
109+ else
110+ [ renderSummary $
111+ "Found " <>
112+ show (Array.length state.results) <>
113+ " definitions" ] <>
114+
115+ [ HH.div [ HP.id_ "spage-search-results-container" ] $
116+ Array.concat $
117+ state.results <#> renderResult
90118 ]
91− if Array.null state.results
92− then
93− [ renderSummary "No search results" ]
94− else
95− [ renderSummary $
96− "Found " <>
97− show (Array.length state.results) <>
98− " definitions" ] <>
99−
100− [ HH.div_ $ state.results <#> \result ->
101− HH.div_ [ HH.text (unwrap result).name ]
102− ]
103119
104120renderSummary :: forall a b. String -> HH.HTML b a
105121renderSummary text =
106122 HH.div [ HP.id_ "spago-search-summary" ]
107123 [ HH.text text
108124 ]
125+
126+renderResult :: forall a b. SearchResult -> Array (HH.HTML a b)
127+renderResult = unwrap >>> \result ->
128+ [ HH.div [ HP.class_ (wrap "result") ]
129+ [ HH.h3 [ HP.class_ (wrap "result__title") ]
130+ [ HH.a [ HP.class_ (wrap "result__link") ]
131+ [ HH.text result.name ]
132+ ]
133+ ]
134+
135+ , HH.div [ HP.class_ (wrap "result__body") ] $
136+ [ HH.pre [ HP.class_ (wrap "result__signature") ]
137+ [ HH.code_ []
138+ ]
139+ ] <>
140+
141+ case result.comments of
142+ Just comments -> [ HH.pre_
143+ [ HH.text comments
144+ ]
145+ ]
146+ Nothing -> [ ]
147+
148+ , HH.div [ HP.class_ (wrap "result__actions") ]
149+ [ HH.span [ HP.class_ (wrap "result__actions__item") ]
150+ [ HH.span [ HP.classes [ wrap "badge"
151+ , wrap "badge--package"
152+ ]
153+ , HP.title "Package"
154+ ]
155+ [ HH.text "P"
156+ ]
157+ , HH.text result.packageName
158+ ]
159+
160+ , HH.span [ HP.class_ (wrap "result__actions__item") ]
161+ [ HH.span [ HP.classes [ wrap "badge"
162+ , wrap "badge--module"
163+ ]
164+ , HP.title "Module"
165+ ]
166+ [ HH.text "M"
167+ ]
168+ , HH.text result.moduleName
169+ ]
170+ ]
171+ ]
src/Spago/Search/Declarations.purs+18−8
@@ -2,17 +2,21 @@ module Spago.Search.Declarations where
22
33import Prelude
44
5−import Effect
6−import Effect.Aff
7−import Control.Promise
8−import Data.Argonaut.Decode (class DecodeJson, decodeJson)
5+import Control.Promise (Promise, toAffE)
6+import Data.Argonaut.Core (Json)
7+import Data.Argonaut.Decode (class DecodeJson, decodeJson, (.:), (.:?))
98import Data.Argonaut.Encode (class EncodeJson, encodeJson)
9+import Data.Either (Either(..))
1010import Data.Generic.Rep (class Generic)
1111import Data.Generic.Rep.Show (genericShow)
12+import Data.Maybe (Maybe)
1213import Data.Newtype (class Newtype, unwrap)
14+import Effect (Effect)
15+import Effect.Aff (Aff)
1316
1417newtype IndexEntry
1518 = IndexEntry { title :: String
19+ , comments :: Maybe String
1620 , info :: { declType :: String
1721 }
1822 , sourceSpan :: { start :: Array Int
@@ -29,7 +33,13 @@ instance showIndexEntry :: Show IndexEntry where
2933 show = genericShow
3034
3135instance decodeJsonIndexEntry :: DecodeJson IndexEntry where
32− decodeJson json = IndexEntry <$> decodeJson json
36+ decodeJson json = IndexEntry <$> do
37+ handle <- decodeJson json
38+ title <- handle .: "title"
39+ comments <- handle .:? "comments"
40+ info <- handle .: "info"
41+ sourceSpan <- handle .: "sourceSpan"
42+ pure { title, comments, info, sourceSpan }
3343
3444instance encodeJsonIndexEntry :: EncodeJson IndexEntry where
3545 encodeJson = encodeJson <<< unwrap
@@ -52,7 +62,7 @@ instance decodeJsonDeclarations :: DecodeJson Declarations where
5262instance encodeJsonDeclarations :: EncodeJson Declarations where
5363 encodeJson = encodeJson <<< unwrap
5464
55−loadDeclarations :: String -> Aff (Array Declarations)
56−loadDeclarations = toAffE <<< loadDeclarations_
65+loadDeclarations :: String -> Aff (Either String (Array Declarations))
66+loadDeclarations string = decodeJson <$> toAffE (loadDeclarations_ string)
5767
58−foreign import loadDeclarations_ :: String -> Effect (Promise (Array Declarations))
68+foreign import loadDeclarations_ :: String -> Effect (Promise Json)
src/Spago/Search/Index.purs+44−9
@@ -5,9 +5,12 @@ import Prelude
55import Spago.Search.Declarations (Declarations(..), IndexEntry(..))
66
77import Data.Foldable (foldr)
8+import Data.Array ((!!))
89import Data.List (List)
910import Data.List as List
10−import Data.Maybe (Maybe(..))
11+import Data.Maybe (Maybe(..), fromMaybe)
12+import Data.String.Common as String
13+import Data.String.Pattern (Pattern(..))
1114import Data.Newtype (class Newtype)
1215import Data.Search.Trie (Trie, alter)
1316import Data.String.CodeUnits (toCharArray)
@@ -19,6 +22,14 @@ derive instance newtypeSearchIndex :: Newtype SearchIndex _
1922
2023newtype SearchResult
2124 = SearchResult { name :: String
25+ , comments :: Maybe String
26+ , declType :: String
27+ , moduleName :: String
28+ , packageName :: String
29+ , sourceSpan :: { start :: Array Int
30+ , end :: Array Int
31+ , name :: String
32+ }
2233 }
2334
2435derive instance newtypeSearchResult :: Newtype SearchResult _
@@ -27,10 +38,11 @@ mkSearchIndex :: Array Declarations -> SearchIndex
2738mkSearchIndex = SearchIndex <<< foldr insertDeclarations mempty
2839 where
2940 insertDeclarations (Declarations { name, declarations }) trie
30− = foldr insertIndexEntry trie declarations
41+ = foldr (insertIndexEntry name) trie declarations
3142
32− insertIndexEntry entry@(IndexEntry { title }) trie
33− = foldr insertSearchResult trie (resultsForEntry entry)
43+ insertIndexEntry moduleName entry@(IndexEntry { title }) trie
44+ = foldr insertSearchResult trie
45+ (resultsForEntry moduleName entry)
3446
3547 insertSearchResult :: { path :: String
3648 , result :: SearchResult
@@ -46,12 +58,35 @@ mkSearchIndex = SearchIndex <<< foldr insertDeclarations mempty
4658 Nothing ->
4759 Just $ List.singleton result
4860
49− resultsForEntry :: IndexEntry -> List { path :: String
50− , result :: SearchResult
51− }
52− resultsForEntry (IndexEntry { title })
61+ resultsForEntry
62+ :: String
63+ -> IndexEntry
64+ -> List { path :: String
65+ , result :: SearchResult
66+ }
67+ resultsForEntry moduleName (IndexEntry { title
68+ , comments
69+ , info
70+ , sourceSpan })
5371 = List.fromFoldable
5472 [ { path: title
55− , result : SearchResult { name: title }
73+ , result: SearchResult { name: title
74+ , comments
75+ , declType: info.declType
76+ , moduleName
77+ , sourceSpan
78+ , packageName:
79+ extractPackageName sourceSpan
80+ }
5681 }
5782 ]
83+
84+ extractPackageName { name } =
85+ let chunks = String.split (Pattern "/") name in
86+ fromMaybe "<unknown>" $
87+ chunks !! 0 >>= \dir ->
88+ -- TODO: is is safe to assume that directory name is ".spago"?
89+ if dir == ".spago" then
90+ chunks !! 1
91+ else
92+ Just "<local package>"
src/Spago/Search/IndexBuilder.purs+22−9
@@ -10,14 +10,14 @@ import Data.Argonaut.Decode (decodeJson)
1010import Data.Argonaut.Encode (encodeJson)
1111import Data.Argonaut.Parser (jsonParser)
1212import Data.Array as Array
13−import Data.Either (hush)
13+import Data.Either (hush, isLeft)
1414import Data.Maybe (Maybe(..))
1515import Data.Newtype (unwrap)
1616import Data.Search.Trie as Trie
17−import Data.String.CodePoints as String
18−import Data.String.Common as String
17+import Data.String.CodePoints (contains) as String
18+import Data.String.Common (replace) as String
1919import Data.String.Pattern (Pattern(..), Replacement(..))
20−import Data.String.Pattern as String
20+import Data.String.Pattern (Pattern(..)) as String
2121import Data.Traversable (for, for_)
2222import Effect (Effect)
2323import Effect.Aff (Aff, launchAff_)
@@ -28,15 +28,28 @@ import Node.FS.Aff (exists, readTextFile, readdir, writeTextFile)
2828import Node.Process as Process
2929
3030collectDeclarations :: Array String -> Aff (Array Declarations)
31−collectDeclarations paths =
32− Array.catMaybes <$> for paths \path -> do
33− let jsonFile = "output/" <> path <> "/docs.json"
31+collectDeclarations paths = do
32+ mbs <- for paths \moduleName -> do
33+ let jsonFile = "output/" <> moduleName <> "/docs.json"
3434 doesExist <- exists jsonFile
35+
3536 if doesExist then do
3637 contents <- readTextFile UTF8 jsonFile
37− pure $ hush $ jsonParser contents >>= decodeJson
38− else
38+ let mbResult = jsonParser contents >>= decodeJson
39+
40+ when (isLeft mbResult) do
41+ liftEffect $ log $
42+ "Index build failed for module " <>
43+ moduleName <>
44+ ": " <> show mbResult
45+
46+ pure $ hush mbResult
47+ else do
48+ liftEffect $ do
49+ log $
50+ "Couldn't find docs.json for " <> moduleName
3951 pure Nothing
52+ pure $ Array.catMaybes mbs
4053
4154writeDeclarations :: Array Declarations -> Aff Unit
4255writeDeclarations decls = do