1#if __GLASGOW_HASKELL__ >= 800
2{-# OPTIONS_GHC -Wno-orphans #-}
6-- Module : Streamly.Internal.Data.Parser
7-- Copyright : (c) 2020 Composewell Technologies
8-- License : BSD-3-Clause
9-- Maintainer : streamly@composewell.com
10-- Stability : experimental
13-- Fast backtracking parsers with stream fusion and native streaming
16-- 'Applicative' and 'Control.Applicative.Alternative' type class based
17-- combinators from the
18-- <http://hackage.haskell.org/package/parser-combinators parser-combinators>
19-- package can also be used with the 'Parser' type. However, there are two
20-- important differences between @parser-combinators@ and the equivalent ones
21-- provided in this module in terms of performance:
23-- 1) @parser-combinators@ use plain Haskell lists to collect the results, in a
24-- strict Monad like IO, the results are necessarily buffered before they can
25-- be consumed. This may not perform optimally in streaming applications
26-- processing large amounts of data. Equivalent combinators in this module can
27-- consume the results of parsing using a 'Fold', thus providing a scalability
28-- and a composable consumer.
30-- 2) Several combinators in this module can be many times faster because of
31-- stream fusion. For example, 'Streamly.Internal.Data.Parser.many' combinator
32-- in this module is much faster than the 'Control.Applicative.many' combinator
33-- of 'Control.Applicative.Alternative' type class.
37-- Failing parsers in this module throw the 'D.ParseError' exception.
41-- As far as possible, try that the names of the combinators in this module are
44-- * <https://hackage.haskell.org/package/base/docs/Text-ParserCombinators-ReadP.html base/Text.ParserCombinators.ReadP>
45-- * <http://hackage.haskell.org/package/parser-combinators parser-combinators>
46-- * <http://hackage.haskell.org/package/megaparsec megaparsec>
47-- * <http://hackage.haskell.org/package/attoparsec attoparsec>
48-- * <http://hackage.haskell.org/package/parsec parsec>
50module Streamly.Internal.Data.Parser
56 -- First order parsers
73 -- | Parsers chained in series, if one parser terminates the composition
76 -- TODO: Currently we are using folds to collect the output of the parsers
77 -- but we can use Parsers instead of folds to make the composition more
78 -- powerful. For example, we can do:
81 -- sliceSepByMax cond n p = sliceBy cond (take n p)
82 -- sliceSepByBetween cond m n p = sliceBy cond (takeBetween m n p)
83 -- takeWhileBetween cond m n p = takeWhile cond (takeBetween m n p)
86 -- TODO: Like toList fold we can have toNonEmpty Parser to fold to a
87 -- nonempty list. If we cannot collect even one element the parser will
88 -- fail. toNonEmpty can be used in takeWhile to implement takeWhile1.
90 -- | Grab a sequence of input elements without inspecting them
97 -- , takeLE -- take -- takeBetween 0 n
98 -- , takeLE1 -- take1 -- takeBetween 1 n
100 , takeEQ -- takeBetween n n
101 , takeGE -- takeBetween n maxBound
103 -- Grab a sequence of input elements by inspecting them
113 -- , sliceSepByBetween
122 -- , frameSepBy -- parse frames escaped by an escape char/sequence
131 -- , prefixOf -- match any prefix of a given string
132 -- , suffixOf -- match any suffix of a given string
133 -- , infixOf -- match any substring of a given string
136 -- Second order parsers (parsers using parsers)
137 -- * Binary Combinators
139 -- ** Sequential Applicative
143 -- ** Parallel Applicatives
147 -- , teeTill -- like manyTill but parallel
149 -- ** Sequential Interleaving
150 -- Use two folds, run a primary parser, its rejected values go to the
154 -- ** Sequential Alternative
157 -- ** Parallel Alternatives
162 -- * N-ary Combinators
163 -- ** Sequential Collection
167 -- ** Sequential Repetition
170 -- , countBetweenTill
177 -- | TODO: traditional implmentations of these may be of limited use. For
178 -- example, consider parsing lines separated by @\\r\\n@. The main parser
179 -- will have to detect and exclude the sequence @\\r\\n@ anyway so that we
180 -- can apply the "sep" parser.
182 -- We can instead implement these as special cases of deintercalate.
195 -- | A simple and stupid impl would be to just convert the stream to an
196 -- array and give the array reference to all consumers. The array can be
197 -- grown on demand by any consumer and truncated when nonbody needs it.
199 -- ** Distribute to collection
200 -- ** Distribute to repetition
202 -- ** Interleaved collection
208 -- ** Interleaved repetition
209 -- | repeat one parser and when it fails run an error recovery parser
210 -- e.g. to find a key frame in the stream after an error
212 -- ** Collection of Alternatives
218 -- , fastestN -- first N successful in time
219 -- , choiceN -- first N successful in position
221 , choice -- first successful in position
223 -- ** Repeated Alternatives
227 -- , retryMax -- try N times
228 -- , retryUntil -- try until successful
229 -- , retryUntilN -- try until successful n times
234import Control.Monad.Catch (MonadCatch)
236 hiding (any, all, take, takeWhile, sequence, concatMap)
238import Streamly.Internal.Data.Fold.Types (Fold(..))
239import Streamly.Internal.Data.Parser.ParserK.Types (Parser)
241import qualified Streamly.Internal.Data.Parser.ParserD as D
242import qualified Streamly.Internal.Data.Parser.ParserK.Types as K
244-------------------------------------------------------------------------------
245-- Upgrade folds to parses
246-------------------------------------------------------------------------------
248-- | Make a 'Parser' from a 'Fold'.
252{-# INLINE fromFold #-}
253fromFold :: MonadCatch m => Fold m a b -> Parser m a b
254fromFold = D.toParserK . D.fromFold
256-------------------------------------------------------------------------------
257-- Terminating but not failing folds
258-------------------------------------------------------------------------------
261-- >>> S.parse (PR.any (== 0)) $ S.fromList [1,0,1]
265any :: MonadCatch m => (a -> Bool) -> Parser m a Bool
266any = D.toParserK . D.any
269-- >>> S.parse (PR.all (== 0)) $ S.fromList [1,0,1]
273all :: MonadCatch m => (a -> Bool) -> Parser m a Bool
274all = D.toParserK . D.all
276-- This is the dual of stream "yield".
278-- | A parser that always yields a pure value without consuming any input.
282{-# INLINE [3] yield #-}
283yield :: MonadCatch m => b -> Parser m a b
284yield = D.toParserK . D.yield
285{-# RULES "yield fallback to CPS" [2]
286 forall a. D.toParserK (D.yield a) = K.yield a #-}
288-- This is the dual of stream "yieldM".
290-- | A parser that always yields the result of an effectful action without
291-- consuming any input.
296yieldM :: MonadCatch m => m b -> Parser m a b
297yieldM = D.toParserK . D.yieldM
299-- This is the dual of "nil".
301-- | A parser that always fails with an error message without consuming
306{-# INLINE [3] die #-}
307die :: MonadCatch m => String -> Parser m a b
308die = D.toParserK . D.die
309{-# RULES "die fallback to CPS" [2]
310 forall a. D.toParserK (D.die a) = K.die a #-}
312-- This is the dual of "nilM".
314-- | A parser that always fails with an effectful error message and without
315-- consuming any input.
320dieM :: MonadCatch m => m String -> Parser m a b
321dieM = D.toParserK . D.dieM
323-------------------------------------------------------------------------------
325-------------------------------------------------------------------------------
327-- | Peek the head element of a stream, without consuming it. Fails if it
328-- encounters end of input.
330-- >>> S.parse ((,) <$> PR.peek <*> PR.satisfy (> 0)) $ S.fromList [1]
334-- peek = lookAhead (satisfy True)
340peek :: MonadCatch m => Parser m a a
341peek = D.toParserK D.peek
343-- | Succeeds if we are at the end of input, fails otherwise.
345-- >>> S.parse ((,) <$> PR.satisfy (> 0) <*> PR.eof) $ S.fromList [1]
351eof :: MonadCatch m => Parser m a ()
352eof = D.toParserK D.eof
354-- | Returns the next element if it passes the predicate, fails otherwise.
356-- >>> S.parse (PR.satisfy (== 1)) $ S.fromList [1,0,1]
361{-# INLINE satisfy #-}
362satisfy :: MonadCatch m => (a -> Bool) -> Parser m a a
363satisfy = D.toParserK . D.satisfy
365-------------------------------------------------------------------------------
367-------------------------------------------------------------------------------
370-- Note: this is called takeP in some parser libraries.
372-- TODO Once we have terminating folds, this Parse should get replaced by Fold.
373-- Alternatively, we can name it "chunkOf" and the corresponding time domain
374-- combinator as "intervalOf" or even "chunk" and "interval".
376-- | Take at most @n@ input elements and fold them using the supplied fold.
378-- Stops after @n@ elements.
381-- >>> S.parse (PR.take 1 FL.toList) $ S.fromList [1]
385-- S.chunksOf n f = S.splitParse (FL.take n f)
391take :: MonadCatch m => Int -> Fold m a b -> Parser m a b
392take n = D.toParserK . D.take n
394-- | Stops after taking exactly @n@ input elements.
396-- * Stops - after @n@ elements.
397-- * Fails - if the stream ends before it can collect @n@ elements.
399-- >>> S.parse (PR.takeEQ 4 FL.toList) $ S.fromList [1,0,1]
400-- > "takeEQ: Expecting exactly 4 elements, got 3"
405takeEQ :: MonadCatch m => Int -> Fold m a b -> Parser m a b
406takeEQ n = D.toParserK . D.takeEQ n
408-- | Take at least @n@ input elements, but can collect more.
411-- * Fails - if the stream end before producing @n@ elements.
413-- >>> S.parse (PR.takeGE 4 FL.toList) $ S.fromList [1,0,1]
414-- > "takeGE: Expecting at least 4 elements, got only 3"
416-- >>> S.parse (PR.takeGE 4 FL.toList) $ S.fromList [1,0,1,0,1]
422takeGE :: MonadCatch m => Int -> Fold m a b -> Parser m a b
423takeGE n = D.toParserK . D.takeGE n
426-- Note: This is called @takeWhileP@ and @munch@ in some parser libraries.
428-- | Collect stream elements until an element fails the predicate. The element
429-- on which the predicate fails is returned back to the input stream.
431-- * Stops - when the predicate fails.
434-- >>> S.parse (PR.takeWhile (== 0) FL.toList) $ S.fromList [0,0,1,0,1]
437-- We can implement a @breakOn@ using 'takeWhile':
440-- breakOn p = takeWhile (not p)
445{-# INLINE takeWhile #-}
446takeWhile :: MonadCatch m => (a -> Bool) -> Fold m a b -> Parser m a b
447takeWhile cond = D.toParserK . D.takeWhile cond
449-- | Like 'takeWhile' but takes at least one element otherwise fails.
453{-# INLINE takeWhile1 #-}
454takeWhile1 :: MonadCatch m => (a -> Bool) -> Fold m a b -> Parser m a b
455takeWhile1 cond = D.toParserK . D.takeWhile1 cond
457-- | Collect stream elements until an element succeeds the predicate. Drop the
458-- element on which the predicate succeeded. The succeeding element is treated
459-- as an infix separator which is dropped from the output.
461-- * Stops - when the predicate succeeds.
464-- >>> S.parse (PR.sliceSepBy (== 1) FL.toList) $ S.fromList [0,0,1,0,1]
467-- S.splitOn pred f = S.splitParse (PR.sliceSepBy pred f)
469-- >>> S.toList $ S.splitParse (PR.sliceSepBy (== 1) FL.toList) $ S.fromList [0,0,1,0,1]
474{-# INLINABLE sliceSepBy #-}
475sliceSepBy :: MonadCatch m => (a -> Bool) -> Fold m a b -> Parser m a b
476sliceSepBy cond = D.toParserK . D.sliceSepBy cond
478-- | Collect stream elements until an element succeeds the predicate. Also take
479-- the element on which the predicate succeeded. The succeeding element is
480-- treated as a suffix separator which is kept in the output segement.
482-- * Stops - when the predicate succeeds.
485-- S.splitWithSuffix pred f = S.splitParse (PR.sliceEndWith pred f)
489{-# INLINABLE sliceEndWith #-}
492 (a -> Bool) -> Fold m a b -> Parser m a b
493sliceEndWith = undefined
495-- | Collect stream elements until an elements passes the predicate, return the
496-- last element on which the predicate succeeded back to the input stream. If
497-- the predicate succeeds on the first element itself then it is kept in the
498-- stream and we continue collecting. The succeeding element is treated as a
499-- prefix separator which is kept in the output segement.
501-- * Stops - when the predicate succeeds in non-leading position.
504-- S.splitWithPrefix pred f = S.splitParse (PR.sliceBeginWith pred f)
508{-# INLINABLE sliceBeginWith #-}
511 (a -> Bool) -> Fold m a b -> Parser m a b
512sliceBeginWith = undefined
514-- | Split using a condition or a count whichever occurs first. This is a
515-- hybrid of 'splitOn' and 'take'. The element on which the condition succeeds
520{-# INLINABLE sliceSepByMax #-}
521sliceSepByMax :: MonadCatch m
522 => (a -> Bool) -> Int -> Fold m a b -> Parser m a b
523sliceSepByMax cond cnt = D.toParserK . D.sliceSepByMax cond cnt
525-- | Like 'splitOn' but strips leading, trailing, and repeated separators.
526-- Therefore, @".a..b."@ having '.' as the separator would be parsed as
527-- @["a","b"]@. In other words, its like parsing words from whitespace
530-- * Stops - when it finds a word separator after a non-word element
534-- S.wordsBy pred f = S.splitParse (PR.wordBy pred f)
539{-# INLINABLE wordBy #-}
542 (a -> Bool) -> Fold m a b -> Parser m a b
545-- | @groupBy cmp f $ S.fromList [a,b,c,...]@ assigns the element @a@ to the
546-- first group, then if @a \`cmp` b@ is 'True' @b@ is also assigned to the same
547-- group. If @a \`cmp` c@ is 'True' then @c@ is also assigned to the same
548-- group and so on. When the comparison fails a new group is started. Each
549-- group is folded using the 'Fold' @f@ and the result of the fold is emitted
550-- in the output stream.
552-- * Stops - when the comparison fails.
556-- S.groupsBy cmp f = S.splitParse (PR.groupBy cmp f)
561{-# INLINABLE groupBy #-}
564 (a -> a -> Bool) -> Fold m a b -> Parser m a b
567-- | Match the given sequence of elements using the given comparison function.
569-- >>> S.parse $ S.eqBy (==) "string" $ S.fromList "string"
571-- >>> S.parse $ S.eqBy (==) "mismatch" $ S.fromList "match"
572-- > *** Exception: ParseError "eqBy: failed, yet to match 7 elements"
577eqBy :: MonadCatch m => (a -> a -> Bool) -> [a] -> Parser m a ()
578eqBy cmp = D.toParserK . D.eqBy cmp
580-------------------------------------------------------------------------------
582-------------------------------------------------------------------------------
584-- | Sequential parser application. Apply two parsers sequentially to an input
585-- stream. The input is provided to the first parser, when it is done the
586-- remaining input is provided to the second parser. If both the parsers
587-- succeed their outputs are combined using the supplied function. The
588-- operation fails if any of the parsers fail.
590-- Note: This is a parsing dual of appending streams using
591-- 'Streamly.Prelude.serial', it splits the streams using two parsers and zips
594-- This implementation is strict in the second argument, therefore, the
595-- following will fail:
597-- >>> S.parse (PR.splitWith const (PR.satisfy (> 0)) undefined) $ S.fromList [1]
599-- Compare with 'Applicative' instance method '<*>'. This implementation allows
600-- stream fusion but has quadratic complexity. This can fuse with other
601-- operations and can be faster than 'Applicative' instance for small number
602-- (less than 8) of compositions.
606{-# INLINE splitWith #-}
607splitWith :: MonadCatch m
608 => (a -> b -> c) -> Parser m x a -> Parser m x b -> Parser m x c
610 D.toParserK $ D.splitWith f (D.fromParserK p1) (D.fromParserK p2)
612-- | Sequential parser application ignoring the output of the first parser.
613-- Apply two parsers sequentially to an input stream. The input is provided to
614-- the first parser, when it is done the remaining input is provided to the
615-- second parser. The output of the parser is the output of the second parser.
616-- The operation fails if any of the parsers fail.
618-- This implementation is strict in the second argument, therefore, the
619-- following will fail:
621-- >>> S.parse (split_ (PR.satisfy (> 0)) undefined) $ S.fromList [1]
623-- Compare with 'Applicative' instance method '*>'. This implementation allows
624-- stream fusion but has quadratic complexity. This can fuse with other
625-- operations, and can be faster than 'Applicative' instance for small
626-- number (less than 8) of compositions.
631split_ :: MonadCatch m => Parser m x a -> Parser m x b -> Parser m x b
632split_ p1 p2 = D.toParserK $ D.split_ (D.fromParserK p1) (D.fromParserK p2)
634-- | @teeWith f p1 p2@ distributes its input to both @p1@ and @p2@ until both
635-- of them succeed or anyone of them fails and combines their output using @f@.
636-- The parser succeeds if both the parsers succeed.
640{-# INLINE teeWith #-}
641teeWith :: MonadCatch m
642 => (a -> b -> c) -> Parser m x a -> Parser m x b -> Parser m x c
644 D.toParserK $ D.teeWith f (D.fromParserK p1) (D.fromParserK p2)
646-- | Like 'teeWith' but ends parsing and zips the results, if available,
647-- whenever the first parser ends.
651{-# INLINE teeWithFst #-}
652teeWithFst :: MonadCatch m
653 => (a -> b -> c) -> Parser m x a -> Parser m x b -> Parser m x c
655 D.toParserK $ D.teeWithFst f (D.fromParserK p1) (D.fromParserK p2)
657-- | Like 'teeWith' but ends parsing and zips the results, if available,
658-- whenever any of the parsers ends or fails.
662{-# INLINE teeWithMin #-}
663teeWithMin :: MonadCatch m
664 => (a -> b -> c) -> Parser m x a -> Parser m x b -> Parser m x c
666 D.toParserK $ D.teeWithMin f (D.fromParserK p1) (D.fromParserK p2)
668-- | Sequential alternative. Apply the input to the first parser and return the
669-- result if the parser succeeds. If the first parser fails then backtrack and
670-- apply the same input to the second parser and return the result.
672-- Note: This implementation is not lazy in the second argument. The following
675-- >>> S.parse (PR.satisfy (> 0) `PR.alt` undefined) $ S.fromList [1..10]
677-- Compare with 'Alternative' instance method '<|>'. This implementation allows
678-- stream fusion but has quadratic complexity. This can fuse with other
679-- operations and can be much faster than 'Alternative' instance for small
680-- number (less than 8) of alternatives.
685alt :: MonadCatch m => Parser m x a -> Parser m x a -> Parser m x a
686alt p1 p2 = D.toParserK $ D.alt (D.fromParserK p1) (D.fromParserK p2)
688-- | Shortest alternative. Apply both parsers in parallel but choose the result
689-- from the one which consumed least input i.e. take the shortest succeeding
694{-# INLINE shortest #-}
695shortest :: MonadCatch m
696 => Parser m x a -> Parser m x a -> Parser m x a
697shortest p1 p2 = D.toParserK $ D.shortest (D.fromParserK p1) (D.fromParserK p2)
699-- | Longest alternative. Apply both parsers in parallel but choose the result
700-- from the one which consumed more input i.e. take the longest succeeding
705{-# INLINE longest #-}
706longest :: MonadCatch m
707 => Parser m x a -> Parser m x a -> Parser m x a
708longest p1 p2 = D.toParserK $ D.longest (D.fromParserK p1) (D.fromParserK p2)
710-- | Run a parser without consuming the input.
714{-# INLINE lookAhead #-}
715lookAhead :: MonadCatch m => Parser m a b -> Parser m a b
716lookAhead p = D.toParserK $ D.lookAhead $ D.fromParserK p
718-------------------------------------------------------------------------------
720-------------------------------------------------------------------------------
722-- To deinterleave we can chain two parsers one behind the other. The input is
723-- given to the first parser and the input definitively rejected by the first
724-- parser is given to the second parser.
726-- We can either have the parsers themselves buffer the input or use the shared
727-- global buffer to hold it until none of the parsers need it. When the first
728-- parser returns Skip (i.e. rewind) we let the second parser consume the
729-- rejected input and when it is done we move the cursor forward to the first
730-- parser again. This will require a "move forward" command as well.
732-- To implement grep we can use three parsers, one to find the pattern, one
733-- to store the context behind the pattern and one to store the context in
734-- front of the pattern. When a match occurs we need to emit the accumulator of
735-- all the three parsers. One parser can count the line numbers to provide the
738-- | Apply two parsers alternately to an input stream. The input stream is
739-- considered an interleaving of two patterns. The two parsers represent the
742-- This undoes a "gintercalate" of two streams.
746{-# INLINE deintercalate #-}
749 Fold m a y -> Parser m x a
750 -> Fold m b z -> Parser m x b
752deintercalate = undefined
754-------------------------------------------------------------------------------
755-- Sequential Collection
756-------------------------------------------------------------------------------
758-- | @sequence f t@ collects sequential parses of parsers in the container @t@
759-- using the fold @f@. Fails if the input ends or any of the parsers fail.
763{-# INLINE sequence #-}
766 Fold m b c -> t (Parser m a b) -> Parser m a c
767sequence _f _p = undefined
769-- | Map a 'Parser' returning function on the result of a 'Parser'.
771-- Compare with 'Monad' instance method '>>='. This implementation allows
772-- stream fusion but has quadratic complexity. This can fuse with other
773-- operations and can be much faster than 'Monad' instance for small number
774-- (less than 8) of compositions.
778{-# INLINE concatMap #-}
779concatMap :: MonadCatch m
780 => (b -> Parser m a c) -> Parser m a b -> Parser m a c
781concatMap f p = D.toParserK $ D.concatMap (D.fromParserK . f) (D.fromParserK p)
783-------------------------------------------------------------------------------
784-- Alternative Collection
785-------------------------------------------------------------------------------
787-- | @choice parsers@ applies the @parsers@ in order and returns the first
793 t (Parser m a b) -> Parser m a b
794choice _ps = undefined
796-------------------------------------------------------------------------------
797-- Sequential Repetition
798-------------------------------------------------------------------------------
801-- TODO "many" is essentially a Fold because it cannot fail. So it can be
802-- downgraded to a Fold. Or we can make the return type a Fold instead and
803-- upgrade that to a parser when needed.
805-- | Collect zero or more parses. Apply the parser repeatedly on the input
806-- stream, stop when the parser fails, accumulate zero or more parse results
807-- using the supplied 'Fold'. This parser never fails, in case the first
808-- application of parser fails it returns an empty result.
810-- Compare with 'Control.Applicative.many'.
815many :: MonadCatch m => Fold m b c -> Parser m a b -> Parser m a c
816many f p = D.toParserK $ D.many f (D.fromParserK p)
817-- many = countBetween 0 maxBound
819-- | Collect one or more parses. Apply the supplied parser repeatedly on the
820-- input stream and accumulate the parse results as long as the parser
821-- succeeds, stop when it fails. This parser fails if not even one result is
824-- Compare with 'Control.Applicative.some'.
829some :: MonadCatch m => Fold m b c -> Parser m a b -> Parser m a c
830some f p = D.toParserK $ D.some f (D.fromParserK p)
831-- some f p = many (takeGE 1 f) p
832-- many = countBetween 1 maxBound
834-- | @countBetween m n f p@ collects between @m@ and @n@ sequential parses of
835-- parser @p@ using the fold @f@. Stop after collecting @n@ results. Fails if
836-- the input ends or the parser fails before @m@ results are collected.
840{-# INLINE countBetween #-}
843 Int -> Int -> Fold m b c -> Parser m a b -> Parser m a c
844countBetween _m _n _f = undefined
845-- countBetween m n f p = many (takeBetween m n f) p
847-- | @count n f p@ collects exactly @n@ sequential parses of parser @p@ using
848-- the fold @f@. Fails if the input ends or the parser fails before @n@
849-- results are collected.
856 Int -> Fold m b c -> Parser m a b -> Parser m a c
857count n = countBetween n n
858-- count n f p = many (takeEQ n f) p
860-- | @manyTill f collect test@ tries the parser @test@ on the input, if @test@
861-- fails it backtracks and tries @collect@, after @collect@ succeeds @test@ is
862-- tried again and so on. The parser stops when @test@ succeeds. The output of
863-- @test@ is discarded and the output of @collect@ is accumulated by the
864-- supplied fold. The parser fails if @collect@ fails.
868{-# INLINE manyTill #-}
869manyTill :: MonadCatch m
870 => Fold m b c -> Parser m a b -> Parser m a x -> Parser m a c
872 D.toParserK $ D.manyTill f (D.fromParserK p1) (D.fromParserK p2)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list