2{-# LANGUAGE RankNTypes #-}
3{-# LANGUAGE ScopedTypeVariables #-}
5#if __GLASGOW_HASKELL__ >= 800
6{-# OPTIONS_GHC -Wno-orphans #-}
10-- Module : Streamly.Internal.Data.Parser
11-- Copyright : (c) 2020 Composewell Technologies
12-- License : BSD-3-Clause
13-- Maintainer : streamly@composewell.com
14-- Stability : experimental
17-- Fast backtracking parsers with stream fusion and native streaming
20-- 'Applicative' and 'Control.Applicative.Alternative' type class based
21-- combinators from the
22-- <http://hackage.haskell.org/package/parser-combinators parser-combinators>
23-- package can also be used with the 'Parser' type. However, there are two
24-- important differences between @parser-combinators@ and the equivalent ones
25-- provided in this module in terms of performance:
27-- 1) @parser-combinators@ use plain Haskell lists to collect the results, in a
28-- strict Monad like IO, the results are necessarily buffered before they can
29-- be consumed. This may not perform optimally in streaming applications
30-- processing large amounts of data. Equivalent combinators in this module can
31-- consume the results of parsing using a 'Fold', thus providing a scalability
32-- and a composable consumer.
34-- 2) Several combinators in this module can be many times faster because of
35-- stream fusion. For example, 'Streamly.Internal.Data.Parser.many' combinator
36-- in this module is much faster than the 'Control.Applicative.many' combinator
37-- of 'Control.Applicative.Alternative' type class.
41-- Failing parsers in this module throw the 'D.ParseError' exception.
45-- As far as possible, try that the names of the combinators in this module are
48-- * <https://hackage.haskell.org/package/base/docs/Text-ParserCombinators-ReadP.html base/Text.ParserCombinators.ReadP>
49-- * <http://hackage.haskell.org/package/parser-combinators parser-combinators>
50-- * <http://hackage.haskell.org/package/megaparsec megaparsec>
51-- * <http://hackage.haskell.org/package/attoparsec attoparsec>
52-- * <http://hackage.haskell.org/package/parsec parsec>
54module Streamly.Internal.Data.Parser
60 -- First order parsers
77 -- | Parsers chained in series, if one parser terminates the composition
80 -- TODO: Currently we are using folds to collect the output of the parsers
81 -- but we can use Parsers instead of folds to make the composition more
82 -- powerful. For example, we can do:
85 -- sliceSepByMax cond n p = sliceBy cond (take n p)
86 -- sliceSepByBetween cond m n p = sliceBy cond (takeBetween m n p)
87 -- takeWhileBetween cond m n p = takeWhile cond (takeBetween m n p)
90 -- TODO: Like toList fold we can have toNonEmpty Parser to fold to a
91 -- nonempty list. If we cannot collect even one element the parser will
92 -- fail. toNonEmpty can be used in takeWhile to implement takeWhile1.
94 -- | Grab a sequence of input elements without inspecting them
101 -- , takeLE -- take -- takeBetween 0 n
102 -- , takeLE1 -- take1 -- takeBetween 1 n
104 , takeEQ -- takeBetween n n
105 , takeGE -- takeBetween n maxBound
107 -- Grab a sequence of input elements by inspecting them
117 -- , sliceSepByBetween
126 -- , frameSepBy -- parse frames escaped by an escape char/sequence
135 -- , prefixOf -- match any prefix of a given string
136 -- , suffixOf -- match any suffix of a given string
137 -- , infixOf -- match any substring of a given string
140 -- Second order parsers (parsers using parsers)
141 -- * Binary Combinators
143 -- ** Sequential Applicative
147 -- ** Parallel Applicatives
151 -- , teeTill -- like manyTill but parallel
153 -- ** Sequential Interleaving
154 -- Use two folds, run a primary parser, its rejected values go to the
158 -- ** Sequential Alternative
161 -- ** Parallel Alternatives
166 -- * N-ary Combinators
167 -- ** Sequential Collection
171 -- ** Sequential Repetition
174 -- , countBetweenTill
181 -- | TODO: traditional implmentations of these may be of limited use. For
182 -- example, consider parsing lines separated by @\\r\\n@. The main parser
183 -- will have to detect and exclude the sequence @\\r\\n@ anyway so that we
184 -- can apply the "sep" parser.
186 -- We can instead implement these as special cases of deintercalate.
199 -- | A simple and stupid impl would be to just convert the stream to an
200 -- array and give the array reference to all consumers. The array can be
201 -- grown on demand by any consumer and truncated when nonbody needs it.
203 -- ** Distribute to collection
204 -- ** Distribute to repetition
206 -- ** Interleaved collection
212 -- ** Interleaved repetition
213 -- | repeat one parser and when it fails run an error recovery parser
214 -- e.g. to find a key frame in the stream after an error
216 -- ** Collection of Alternatives
222 -- , fastestN -- first N successful in time
223 -- , choiceN -- first N successful in position
225 , choice -- first successful in position
227 -- ** Repeated Alternatives
231 -- , retryMax -- try N times
232 -- , retryUntil -- try until successful
233 -- , retryUntilN -- try until successful n times
238import Control.Monad.Catch (MonadCatch)
240 hiding (any, all, take, takeWhile, sequence, concatMap)
242import Streamly.Internal.Data.Fold.Types (Fold(..))
243import Streamly.Internal.Data.Parser.ParserK.Types (Parser)
245import qualified Streamly.Internal.Data.Parser.ParserD as D
246import qualified Streamly.Internal.Data.Parser.ParserK.Types as K
248-------------------------------------------------------------------------------
249-- Upgrade folds to parses
250-------------------------------------------------------------------------------
252-- | Make a 'Parser' from a 'Fold'.
256{-# INLINE fromFold #-}
257fromFold :: MonadCatch m => Fold m a b -> Parser m a b
258fromFold = D.toParserK . D.fromFold
260-------------------------------------------------------------------------------
261-- Terminating but not failing folds
262-------------------------------------------------------------------------------
265-- >>> S.parse (PR.any (== 0)) $ S.fromList [1,0,1]
269any :: MonadCatch m => (a -> Bool) -> Parser m a Bool
270any = D.toParserK . D.any
273-- >>> S.parse (PR.all (== 0)) $ S.fromList [1,0,1]
277all :: MonadCatch m => (a -> Bool) -> Parser m a Bool
278all = D.toParserK . D.all
280-- This is the dual of stream "yield".
282-- | A parser that always yields a pure value without consuming any input.
286{-# INLINE [3] yield #-}
287yield :: MonadCatch m => b -> Parser m a b
288yield = D.toParserK . D.yield
289{-# RULES "yield fallback to CPS" [2]
290 forall a. D.toParserK (D.yield a) = K.yield a #-}
292-- This is the dual of stream "yieldM".
294-- | A parser that always yields the result of an effectful action without
295-- consuming any input.
300yieldM :: MonadCatch m => m b -> Parser m a b
301yieldM = D.toParserK . D.yieldM
303-- This is the dual of "nil".
305-- | A parser that always fails with an error message without consuming
310{-# INLINE [3] die #-}
311die :: MonadCatch m => String -> Parser m a b
312die = D.toParserK . D.die
313{-# RULES "die fallback to CPS" [2]
314 forall a. D.toParserK (D.die a) = K.die a #-}
316-- This is the dual of "nilM".
318-- | A parser that always fails with an effectful error message and without
319-- consuming any input.
324dieM :: MonadCatch m => m String -> Parser m a b
325dieM = D.toParserK . D.dieM
327-------------------------------------------------------------------------------
329-------------------------------------------------------------------------------
331-- | Peek the head element of a stream, without consuming it. Fails if it
332-- encounters end of input.
334-- >>> S.parse ((,) <$> PR.peek <*> PR.satisfy (> 0)) $ S.fromList [1]
338-- peek = lookAhead (satisfy True)
344peek :: MonadCatch m => Parser m a a
345peek = D.toParserK D.peek
347-- | Succeeds if we are at the end of input, fails otherwise.
349-- >>> S.parse ((,) <$> PR.satisfy (> 0) <*> PR.eof) $ S.fromList [1]
355eof :: MonadCatch m => Parser m a ()
356eof = D.toParserK D.eof
358-- | Returns the next element if it passes the predicate, fails otherwise.
360-- >>> S.parse (PR.satisfy (== 1)) $ S.fromList [1,0,1]
365{-# INLINE satisfy #-}
366satisfy :: MonadCatch m => (a -> Bool) -> Parser m a a
367satisfy = D.toParserK . D.satisfy
369-------------------------------------------------------------------------------
371-------------------------------------------------------------------------------
374-- Note: this is called takeP in some parser libraries.
376-- TODO Once we have terminating folds, this Parse should get replaced by Fold.
377-- Alternatively, we can name it "chunkOf" and the corresponding time domain
378-- combinator as "intervalOf" or even "chunk" and "interval".
380-- | Take at most @n@ input elements and fold them using the supplied fold.
382-- Stops after @n@ elements.
385-- >>> S.parse (PR.take 1 FL.toList) $ S.fromList [1]
389-- S.chunksOf n f = S.splitParse (FL.take n f)
395take :: MonadCatch m => Int -> Fold m a b -> Parser m a b
396take n = D.toParserK . D.take n
398-- | Stops after taking exactly @n@ input elements.
400-- * Stops - after @n@ elements.
401-- * Fails - if the stream ends before it can collect @n@ elements.
403-- >>> S.parse (PR.takeEQ 4 FL.toList) $ S.fromList [1,0,1]
404-- > "takeEQ: Expecting exactly 4 elements, got 3"
409takeEQ :: MonadCatch m => Int -> Fold m a b -> Parser m a b
410takeEQ n = D.toParserK . D.takeEQ n
412-- | Take at least @n@ input elements, but can collect more.
415-- * Fails - if the stream end before producing @n@ elements.
417-- >>> S.parse (PR.takeGE 4 FL.toList) $ S.fromList [1,0,1]
418-- > "takeGE: Expecting at least 4 elements, got only 3"
420-- >>> S.parse (PR.takeGE 4 FL.toList) $ S.fromList [1,0,1,0,1]
426takeGE :: MonadCatch m => Int -> Fold m a b -> Parser m a b
427takeGE n = D.toParserK . D.takeGE n
430-- Note: This is called @takeWhileP@ and @munch@ in some parser libraries.
432-- | Collect stream elements until an element fails the predicate. The element
433-- on which the predicate fails is returned back to the input stream.
435-- * Stops - when the predicate fails.
438-- >>> S.parse (PR.takeWhile (== 0) FL.toList) $ S.fromList [0,0,1,0,1]
441-- We can implement a @breakOn@ using 'takeWhile':
444-- breakOn p = takeWhile (not p)
449{-# INLINE takeWhile #-}
450takeWhile :: MonadCatch m => (a -> Bool) -> Fold m a b -> Parser m a b
451takeWhile cond = D.toParserK . D.takeWhile cond
453-- | Like 'takeWhile' but takes at least one element otherwise fails.
457{-# INLINE takeWhile1 #-}
458takeWhile1 :: MonadCatch m => (a -> Bool) -> Fold m a b -> Parser m a b
459takeWhile1 cond = D.toParserK . D.takeWhile1 cond
461-- | Collect stream elements until an element succeeds the predicate. Drop the
462-- element on which the predicate succeeded. The succeeding element is treated
463-- as an infix separator which is dropped from the output.
465-- * Stops - when the predicate succeeds.
468-- >>> S.parse (PR.sliceSepBy (== 1) FL.toList) $ S.fromList [0,0,1,0,1]
471-- S.splitOn pred f = S.splitParse (PR.sliceSepBy pred f)
473-- >>> S.toList $ S.splitParse (PR.sliceSepBy (== 1) FL.toList) $ S.fromList [0,0,1,0,1]
478{-# INLINABLE sliceSepBy #-}
479sliceSepBy :: MonadCatch m => (a -> Bool) -> Fold m a b -> Parser m a b
480sliceSepBy cond = D.toParserK . D.sliceSepBy cond
482-- | Collect stream elements until an element succeeds the predicate. Also take
483-- the element on which the predicate succeeded. The succeeding element is
484-- treated as a suffix separator which is kept in the output segement.
486-- * Stops - when the predicate succeeds.
489-- S.splitWithSuffix pred f = S.splitParse (PR.sliceEndWith pred f)
493{-# INLINABLE sliceEndWith #-}
496 (a -> Bool) -> Fold m a b -> Parser m a b
497sliceEndWith = undefined
499-- | Collect stream elements until an elements passes the predicate, return the
500-- last element on which the predicate succeeded back to the input stream. If
501-- the predicate succeeds on the first element itself then it is kept in the
502-- stream and we continue collecting. The succeeding element is treated as a
503-- prefix separator which is kept in the output segement.
505-- * Stops - when the predicate succeeds in non-leading position.
508-- S.splitWithPrefix pred f = S.splitParse (PR.sliceBeginWith pred f)
512{-# INLINABLE sliceBeginWith #-}
515 (a -> Bool) -> Fold m a b -> Parser m a b
516sliceBeginWith = undefined
518-- | Split using a condition or a count whichever occurs first. This is a
519-- hybrid of 'splitOn' and 'take'. The element on which the condition succeeds
524{-# INLINABLE sliceSepByMax #-}
525sliceSepByMax :: MonadCatch m
526 => (a -> Bool) -> Int -> Fold m a b -> Parser m a b
527sliceSepByMax cond cnt = D.toParserK . D.sliceSepByMax cond cnt
529-- | Like 'splitOn' but strips leading, trailing, and repeated separators.
530-- Therefore, @".a..b."@ having '.' as the separator would be parsed as
531-- @["a","b"]@. In other words, its like parsing words from whitespace
534-- * Stops - when it finds a word separator after a non-word element
538-- S.wordsBy pred f = S.splitParse (PR.wordBy pred f)
543{-# INLINABLE wordBy #-}
546 (a -> Bool) -> Fold m a b -> Parser m a b
549-- | @groupBy cmp f $ S.fromList [a,b,c,...]@ assigns the element @a@ to the
550-- first group, then if @a \`cmp` b@ is 'True' @b@ is also assigned to the same
551-- group. If @a \`cmp` c@ is 'True' then @c@ is also assigned to the same
552-- group and so on. When the comparison fails a new group is started. Each
553-- group is folded using the 'Fold' @f@ and the result of the fold is emitted
554-- in the output stream.
556-- * Stops - when the comparison fails.
560-- S.groupsBy cmp f = S.splitParse (PR.groupBy cmp f)
565{-# INLINABLE groupBy #-}
568 (a -> a -> Bool) -> Fold m a b -> Parser m a b
571-- | Match the given sequence of elements using the given comparison function.
576eqBy :: MonadCatch m => (a -> a -> Bool) -> [a] -> Parser m a ()
577eqBy cmp = D.toParserK . D.eqBy cmp
579-------------------------------------------------------------------------------
581-------------------------------------------------------------------------------
583-- | Sequential parser application. Apply two parsers sequentially to an input
584-- stream. The input is provided to the first parser, when it is done the
585-- remaining input is provided to the second parser. If both the parsers
586-- succeed their outputs are combined using the supplied function. The
587-- operation fails if any of the parsers fail.
589-- Note: This is a parsing dual of appending streams using
590-- 'Streamly.Prelude.serial', it splits the streams using two parsers and zips
593-- This implementation is strict in the second argument, therefore, the
594-- following will fail:
596-- >>> S.parse (PR.splitWith const (PR.satisfy (> 0)) undefined) $ S.fromList [1]
598-- Compare with 'Applicative' instance method '<*>'. This implementation allows
599-- stream fusion but has quadratic complexity. This can fuse with other
600-- operations and can be faster than 'Applicative' instance for small number
601-- (less than 8) of compositions.
605{-# INLINE splitWith #-}
606splitWith :: MonadCatch m
607 => (a -> b -> c) -> Parser m x a -> Parser m x b -> Parser m x c
609 D.toParserK $ D.splitWith f (D.fromParserK p1) (D.fromParserK p2)
611-- | Sequential parser application ignoring the output of the first parser.
612-- Apply two parsers sequentially to an input stream. The input is provided to
613-- the first parser, when it is done the remaining input is provided to the
614-- second parser. The output of the parser is the output of the second parser.
615-- The operation fails if any of the parsers fail.
617-- This implementation is strict in the second argument, therefore, the
618-- following will fail:
620-- >>> S.parse (split_ (PR.satisfy (> 0)) undefined) $ S.fromList [1]
622-- Compare with 'Applicative' instance method '*>'. This implementation allows
623-- stream fusion but has quadratic complexity. This can fuse with other
624-- operations, and can be faster than 'Applicative' instance for small
625-- number (less than 8) of compositions.
630split_ :: MonadCatch m => Parser m x a -> Parser m x b -> Parser m x b
631split_ p1 p2 = D.toParserK $ D.split_ (D.fromParserK p1) (D.fromParserK p2)
633-- | @teeWith f p1 p2@ distributes its input to both @p1@ and @p2@ until both
634-- of them succeed or anyone of them fails and combines their output using @f@.
635-- The parser succeeds if both the parsers succeed.
639{-# INLINE teeWith #-}
640teeWith :: MonadCatch m
641 => (a -> b -> c) -> Parser m x a -> Parser m x b -> Parser m x c
643 D.toParserK $ D.teeWith f (D.fromParserK p1) (D.fromParserK p2)
645-- | Like 'teeWith' but ends parsing and zips the results, if available,
646-- whenever the first parser ends.
650{-# INLINE teeWithFst #-}
651teeWithFst :: MonadCatch m
652 => (a -> b -> c) -> Parser m x a -> Parser m x b -> Parser m x c
654 D.toParserK $ D.teeWithFst f (D.fromParserK p1) (D.fromParserK p2)
656-- | Like 'teeWith' but ends parsing and zips the results, if available,
657-- whenever any of the parsers ends or fails.
661{-# INLINE teeWithMin #-}
662teeWithMin :: MonadCatch m
663 => (a -> b -> c) -> Parser m x a -> Parser m x b -> Parser m x c
665 D.toParserK $ D.teeWithMin f (D.fromParserK p1) (D.fromParserK p2)
667-- | Sequential alternative. Apply the input to the first parser and return the
668-- result if the parser succeeds. If the first parser fails then backtrack and
669-- apply the same input to the second parser and return the result.
671-- Note: This implementation is not lazy in the second argument. The following
674-- >>> S.parse (PR.satisfy (> 0) `PR.alt` undefined) $ S.fromList [1..10]
676-- Compare with 'Alternative' instance method '<|>'. This implementation allows
677-- stream fusion but has quadratic complexity. This can fuse with other
678-- operations and can be much faster than 'Alternative' instance for small
679-- number (less than 8) of alternatives.
684alt :: MonadCatch m => Parser m x a -> Parser m x a -> Parser m x a
685alt p1 p2 = D.toParserK $ D.alt (D.fromParserK p1) (D.fromParserK p2)
687-- | Shortest alternative. Apply both parsers in parallel but choose the result
688-- from the one which consumed least input i.e. take the shortest succeeding
693{-# INLINE shortest #-}
694shortest :: MonadCatch m
695 => Parser m x a -> Parser m x a -> Parser m x a
696shortest p1 p2 = D.toParserK $ D.shortest (D.fromParserK p1) (D.fromParserK p2)
698-- | Longest alternative. Apply both parsers in parallel but choose the result
699-- from the one which consumed more input i.e. take the longest succeeding
704{-# INLINE longest #-}
705longest :: MonadCatch m
706 => Parser m x a -> Parser m x a -> Parser m x a
707longest p1 p2 = D.toParserK $ D.longest (D.fromParserK p1) (D.fromParserK p2)
709-- | Run a parser without consuming the input.
713{-# INLINE lookAhead #-}
714lookAhead :: MonadCatch m => Parser m a b -> Parser m a b
715lookAhead p = D.toParserK $ D.lookAhead $ D.fromParserK p
717-------------------------------------------------------------------------------
719-------------------------------------------------------------------------------
721-- To deinterleave we can chain two parsers one behind the other. The input is
722-- given to the first parser and the input definitively rejected by the first
723-- parser is given to the second parser.
725-- We can either have the parsers themselves buffer the input or use the shared
726-- global buffer to hold it until none of the parsers need it. When the first
727-- parser returns Skip (i.e. rewind) we let the second parser consume the
728-- rejected input and when it is done we move the cursor forward to the first
729-- parser again. This will require a "move forward" command as well.
731-- To implement grep we can use three parsers, one to find the pattern, one
732-- to store the context behind the pattern and one to store the context in
733-- front of the pattern. When a match occurs we need to emit the accumulator of
734-- all the three parsers. One parser can count the line numbers to provide the
737-- | Apply two parsers alternately to an input stream. The input stream is
738-- considered an interleaving of two patterns. The two parsers represent the
741-- This undoes a "gintercalate" of two streams.
745{-# INLINE deintercalate #-}
748 Fold m a y -> Parser m x a
749 -> Fold m b z -> Parser m x b
751deintercalate = undefined
753-------------------------------------------------------------------------------
754-- Sequential Collection
755-------------------------------------------------------------------------------
757-- | @sequence f t@ collects sequential parses of parsers in the container @t@
758-- using the fold @f@. Fails if the input ends or any of the parsers fail.
762{-# INLINE sequence #-}
765 Fold m b c -> t (Parser m a b) -> Parser m a c
766sequence _f _p = undefined
768-- | Map a 'Parser' returning function on the result of a 'Parser'.
770-- Compare with 'Monad' instance method '>>='. This implementation allows
771-- stream fusion but has quadratic complexity. This can fuse with other
772-- operations and can be much faster than 'Monad' instance for small number
773-- (less than 8) of compositions.
777{-# INLINE concatMap #-}
778concatMap :: MonadCatch m
779 => (b -> Parser m a c) -> Parser m a b -> Parser m a c
780concatMap f p = D.toParserK $ D.concatMap (D.fromParserK . f) (D.fromParserK p)
782-------------------------------------------------------------------------------
783-- Alternative Collection
784-------------------------------------------------------------------------------
786-- | @choice parsers@ applies the @parsers@ in order and returns the first
792 t (Parser m a b) -> Parser m a b
793choice _ps = undefined
795-------------------------------------------------------------------------------
796-- Sequential Repetition
797-------------------------------------------------------------------------------
800-- TODO "many" is essentially a Fold because it cannot fail. So it can be
801-- downgraded to a Fold. Or we can make the return type a Fold instead and
802-- upgrade that to a parser when needed.
804-- | Collect zero or more parses. Apply the parser repeatedly on the input
805-- stream, stop when the parser fails, accumulate zero or more parse results
806-- using the supplied 'Fold'. This parser never fails, in case the first
807-- application of parser fails it returns an empty result.
809-- Compare with 'Control.Applicative.many'.
814many :: MonadCatch m => Fold m b c -> Parser m a b -> Parser m a c
815many f p = D.toParserK $ D.many f (D.fromParserK p)
816-- many = countBetween 0 maxBound
818-- | Collect one or more parses. Apply the supplied parser repeatedly on the
819-- input stream and accumulate the parse results as long as the parser
820-- succeeds, stop when it fails. This parser fails if not even one result is
823-- Compare with 'Control.Applicative.some'.
828some :: MonadCatch m => Fold m b c -> Parser m a b -> Parser m a c
829some f p = D.toParserK $ D.some f (D.fromParserK p)
830-- some f p = many (takeGE 1 f) p
831-- many = countBetween 1 maxBound
833-- | @countBetween m n f p@ collects between @m@ and @n@ sequential parses of
834-- parser @p@ using the fold @f@. Stop after collecting @n@ results. Fails if
835-- the input ends or the parser fails before @m@ results are collected.
839{-# INLINE countBetween #-}
842 Int -> Int -> Fold m b c -> Parser m a b -> Parser m a c
843countBetween _m _n _f = undefined
844-- countBetween m n f p = many (takeBetween m n f) p
846-- | @count n f p@ collects exactly @n@ sequential parses of parser @p@ using
847-- the fold @f@. Fails if the input ends or the parser fails before @n@
848-- results are collected.
855 Int -> Fold m b c -> Parser m a b -> Parser m a c
856count n = countBetween n n
857-- count n f p = many (takeEQ n f) p
859-- | @manyTill f collect test@ tries the parser @test@ on the input, if @test@
860-- fails it backtracks and tries @collect@, after @collect@ succeeds @test@ is
861-- tried again and so on. The parser stops when @test@ succeeds. The output of
862-- @test@ is discarded and the output of @collect@ is accumulated by the
863-- supplied fold. The parser fails if @collect@ fails.
867{-# INLINE manyTill #-}
868manyTill :: MonadCatch m
869 => Fold m b c -> Parser m a b -> Parser m a x -> Parser m a c
871 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