Tab completion speedup (#3312)

* Working and fast prefix-based tab complete

* Re-arrange completion modules

* Hash completion for conflicted terms

* Sort Completions

* Remove fuzzy matcher for now

* Remove 'Branch' from suggestion signature

* Add ability to test completions in transcripts

* Use explicit lax-path query parser
This commit is contained in:
Chris Penner 2022-08-16 16:01:46 -05:00 committed by GitHub
parent 723eaf622e
commit 8696fef129
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 704 additions and 339 deletions

View File

@ -1,6 +1,6 @@
cabal-version: 1.12
-- This file has been generated from package.yaml by hpack version 0.34.7.
-- This file has been generated from package.yaml by hpack version 0.34.4.
--
-- see: https://github.com/sol/hpack

View File

@ -1,6 +1,6 @@
cabal-version: 1.12
-- This file has been generated from package.yaml by hpack version 0.34.7.
-- This file has been generated from package.yaml by hpack version 0.34.4.
--
-- see: https://github.com/sol/hpack

View File

@ -3,8 +3,6 @@ cabal-version: 1.12
-- This file has been generated from package.yaml by hpack version 0.34.4.
--
-- see: https://github.com/sol/hpack
--
-- hash: e3307c66c00f5bf45548b61cb0aa78f2006c2df8b9ad9172c645f944688d6263
name: unison-codebase-sync
version: 0.0.0

View File

@ -3,8 +3,6 @@ cabal-version: 1.12
-- This file has been generated from package.yaml by hpack version 0.34.4.
--
-- see: https://github.com/sol/hpack
--
-- hash: c95852b01c310af9965bbec9dfa7637e97148815d674788512bd1748a1858314
name: unison-core
version: 0.0.0

View File

@ -3,8 +3,6 @@ cabal-version: 1.12
-- This file has been generated from package.yaml by hpack version 0.34.4.
--
-- see: https://github.com/sol/hpack
--
-- hash: 065e98437c72cce9c4cc6102d3eb0b3c8be444a95c3c71ab65a9d17f29e95eae
name: unison-util-serialization
version: 0.0.0

View File

@ -3,8 +3,6 @@ cabal-version: 1.12
-- This file has been generated from package.yaml by hpack version 0.34.4.
--
-- see: https://github.com/sol/hpack
--
-- hash: 6b08707c87592d47677b3f2db15c17c94a23c13544f339227e43ac19f7ee7947
name: unison-util-term
version: 0.0.0

View File

@ -38,6 +38,7 @@ module Unison.Codebase
beforeImpl,
shallowBranchAtPath,
getShallowBranchForHash,
getShallowBranchFromRoot,
getShallowRootBranch,
-- * Root branch
@ -145,6 +146,7 @@ import Unison.Reference (Reference)
import qualified Unison.Reference as Reference
import qualified Unison.Referent as Referent
import qualified Unison.Runtime.IOSource as IOSource
import qualified Unison.Sqlite as Sqlite
import Unison.Symbol (Symbol)
import Unison.Term (Term)
import qualified Unison.Term as Term
@ -155,13 +157,16 @@ import qualified Unison.UnisonFile as UF
import qualified Unison.Util.Relation as Rel
import Unison.Var (Var)
import qualified Unison.WatchKind as WK
import qualified Unison.Sqlite as Sqlite
-- | Run a transaction on a codebase.
runTransaction :: MonadIO m => Codebase m v a -> Sqlite.Transaction b -> m b
runTransaction Codebase{withConnection} action =
runTransaction Codebase {withConnection} action =
withConnection \conn -> Sqlite.runTransaction conn action
getShallowBranchFromRoot :: Monad m => Codebase m v a -> Path.Absolute -> m (Maybe (V2Branch.CausalBranch m))
getShallowBranchFromRoot codebase p = do
getShallowRootBranch codebase >>= shallowBranchAtPath (Path.unabsolute p)
-- | Get the shallow representation of the root branches without loading the children or
-- history.
getShallowRootBranch :: Monad m => Codebase m v a -> m (V2.CausalBranch m)

View File

@ -14,7 +14,11 @@ module Unison.Codebase.Path
singleton,
Unison.Codebase.Path.uncons,
empty,
isAbsolute,
isRelative,
absoluteEmpty,
absoluteEmpty',
relativeEmpty,
relativeEmpty',
currentPath,
prefix,
@ -39,7 +43,9 @@ module Unison.Codebase.Path
fromName',
fromPath',
fromText,
fromText',
toAbsoluteSplit,
toSplit',
toList,
toName,
toName',
@ -105,6 +111,14 @@ newtype Relative = Relative {unrelative :: Path} deriving (Eq, Ord)
newtype Path' = Path' {unPath' :: Either Absolute Relative}
deriving (Eq, Ord)
isAbsolute :: Path' -> Bool
isAbsolute (AbsolutePath' _) = True
isAbsolute _ = False
isRelative :: Path' -> Bool
isRelative (RelativePath' _) = True
isRelative _ = False
isCurrentPath :: Path' -> Bool
isCurrentPath p = p == currentPath
@ -172,15 +186,24 @@ prefix (Absolute (Path prefix)) (Path' p) = case p of
Left (unabsolute -> abs) -> abs
Right (unrelative -> rel) -> Path $ prefix <> toSeq rel
toSplit' :: Path' -> Maybe (Path', NameSegment)
toSplit' = Lens.unsnoc
toAbsoluteSplit :: Absolute -> (Path', a) -> (Absolute, a)
toAbsoluteSplit a (p, s) = (resolve a p, s)
absoluteEmpty :: Absolute
absoluteEmpty = Absolute empty
relativeEmpty :: Relative
relativeEmpty = Relative empty
relativeEmpty' :: Path'
relativeEmpty' = Path' (Right (Relative empty))
absoluteEmpty' :: Path'
absoluteEmpty' = Path' (Left (Absolute empty))
-- | Mitchell: this function is bogus, because an empty name segment is bogus
toPath' :: Path -> Path'
toPath' = \case
@ -298,6 +321,25 @@ fromText = \case
"" -> empty
t -> fromList $ NameSegment <$> NameSegment.segments' t
-- | Construct a Path' from a text
--
-- >>> fromText' "a.b.c"
-- a.b.c
--
-- >>> fromText' ".a.b.c"
-- .a.b.c
--
-- >>> show $ fromText' ""
-- ""
fromText' :: Text -> Path'
fromText' txt =
case Text.uncons txt of
Nothing -> relativeEmpty'
Just ('.', p) ->
Path' (Left . Absolute $ fromText p)
Just _ ->
Path' (Right . Relative $ fromText txt)
toText' :: Path' -> Text
toText' = \case
Path' (Left (Absolute path)) -> Text.cons '.' (toText path)
@ -319,6 +361,18 @@ instance Cons Path Path NameSegment NameSegment where
Path (hd :<| tl) -> Right (hd, Path tl)
_ -> Left p
instance Cons Path' Path' NameSegment NameSegment where
_Cons = prism (uncurry cons) uncons
where
cons :: NameSegment -> Path' -> Path'
cons ns (AbsolutePath' p) = AbsolutePath' (ns :< p)
cons ns (RelativePath' p) = RelativePath' (ns :< p)
uncons :: Path' -> Either Path' (NameSegment, Path')
uncons p = case p of
AbsolutePath' (ns :< tl) -> Right (ns, AbsolutePath' tl)
RelativePath' (ns :< tl) -> Right (ns, RelativePath' tl)
_ -> Left p
instance Snoc Relative Relative NameSegment NameSegment where
_Snoc = prism (uncurry snocRelative) $ \case
Relative (Lens.unsnoc -> Just (s, a)) -> Right (Relative s, a)
@ -327,6 +381,26 @@ instance Snoc Relative Relative NameSegment NameSegment where
snocRelative :: Relative -> NameSegment -> Relative
snocRelative r n = Relative . (`Lens.snoc` n) $ unrelative r
instance Cons Relative Relative NameSegment NameSegment where
_Cons = prism (uncurry cons) uncons
where
cons :: NameSegment -> Relative -> Relative
cons ns (Relative p) = Relative (ns :< p)
uncons :: Relative -> Either Relative (NameSegment, Relative)
uncons p = case p of
Relative (ns :< tl) -> Right (ns, Relative tl)
_ -> Left p
instance Cons Absolute Absolute NameSegment NameSegment where
_Cons = prism (uncurry cons) uncons
where
cons :: NameSegment -> Absolute -> Absolute
cons ns (Absolute p) = Absolute (ns :< p)
uncons :: Absolute -> Either Absolute (NameSegment, Absolute)
uncons p = case p of
Absolute (ns :< tl) -> Right (ns, Absolute tl)
_ -> Left p
instance Snoc Absolute Absolute NameSegment NameSegment where
_Snoc = prism (uncurry snocAbsolute) $ \case
Absolute (Lens.unsnoc -> Just (s, a)) -> Right (Absolute s, a)

View File

@ -104,9 +104,11 @@ import qualified Unison.Codebase.TermEdit.Typing as TermEdit
import Unison.Codebase.Type (GitPushBehavior (..))
import qualified Unison.Codebase.TypeEdit as TypeEdit
import qualified Unison.Codebase.Verbosity as Verbosity
import qualified Unison.CommandLine.Completion as Completion
import qualified Unison.CommandLine.DisplayValues as DisplayValues
import qualified Unison.CommandLine.FuzzySelect as Fuzzy
import qualified Unison.CommandLine.InputPattern as InputPattern
import qualified Unison.CommandLine.InputPatterns as IP
import qualified Unison.CommandLine.InputPatterns as InputPatterns
import Unison.ConstructorReference (GConstructorReference (..))
import qualified Unison.DataDeclaration as DD
@ -1386,6 +1388,12 @@ loop e = do
effects = [(Name.unsafeFromVar v, r) | (v, (r, _e)) <- Map.toList $ UF.effectDeclarationsId' uf]
terms = [(Name.unsafeFromVar v, r) | (v, (r, _wk, _tm, _tp)) <- Map.toList $ UF.hashTermsId uf]
Cli.respond $ DumpUnisonFileHashes hqLength datas effects terms
DebugTabCompletionI inputs -> do
Cli.Env {codebase} <- ask
currentPath <- Cli.getCurrentPath
let completionFunc = Completion.haskelineTabComplete IP.patternMap codebase currentPath
(_, completions) <- liftIO $ completionFunc (reverse (unwords inputs), "")
Cli.respond (DisplayDebugCompletions completions)
DebugDumpNamespacesI -> do
let seen h = State.gets (Set.member h)
set h = State.modify (Set.insert h)
@ -1647,6 +1655,7 @@ inputDescription input =
UiI -> wat
UpI {} -> wat
VersionI -> wat
DebugTabCompletionI _input -> wat
where
hp' :: Either SBH.ShortBranchHash Path' -> Cli r Text
hp' = either (pure . Text.pack . show) p'

View File

@ -176,6 +176,7 @@ data Input
| -- | List all external dependencies of a given namespace, or the current namespace if
-- no path is provided.
NamespaceDependenciesI (Maybe Path')
| DebugTabCompletionI [String] -- The raw arguments provided
| DebugNumberedArgsI
| DebugTypecheckedUnisonFileI
| DebugDumpNamespacesI

View File

@ -19,6 +19,7 @@ import Data.List.NonEmpty (NonEmpty)
import qualified Data.Set as Set
import Data.Set.NonEmpty (NESet)
import Network.URI (URI)
import qualified System.Console.Haskeline as Completion
import Unison.Auth.Types (CredentialFailure)
import qualified Unison.Codebase.Branch as Branch
import Unison.Codebase.Editor.DisplayObject (DisplayObject)
@ -256,6 +257,7 @@ data Output
| CredentialFailureMsg CredentialFailure
| PrintVersion Text
| IntegrityCheck IntegrityResult
| DisplayDebugCompletions [Completion.Completion]
data ShareError
= ShareErrorCheckAndSetPush Sync.CheckAndSetPushError
@ -397,6 +399,7 @@ isFailure o = case o of
IntegrityErrorDetected {} -> True
ShareError {} -> True
ViewOnShare {} -> False
DisplayDebugCompletions {} -> False
isNumberedFailure :: NumberedOutput -> Bool
isNumberedFailure = \case

View File

@ -17,17 +17,6 @@ module Unison.CommandLine
warn,
warnNote,
-- * Completers
completion,
completion',
exactComplete,
fuzzyComplete,
fuzzyCompleteHashQualified,
prefixIncomplete,
prettyCompletion,
fixupCompletion,
completeWithinQueryNamespace,
-- * Other
parseInput,
prompt,
@ -43,12 +32,9 @@ import Data.Configurator (autoConfig, autoReload)
import qualified Data.Configurator as Config
import Data.Configurator.Types (Config, Worth (..))
import Data.List (isPrefixOf, isSuffixOf)
import qualified Data.List as List
import Data.List.Extra (nubOrd)
import Data.ListLike (ListLike)
import qualified Data.Map as Map
import qualified Data.Text as Text
import qualified System.Console.Haskeline as Line
import System.FilePath (takeFileName)
import Text.Regex.TDFA ((=~))
import Unison.Codebase.Branch (Branch0)
@ -58,13 +44,8 @@ import qualified Unison.Codebase.Watch as Watch
import qualified Unison.CommandLine.Globbing as Globbing
import Unison.CommandLine.InputPattern (InputPattern (..))
import qualified Unison.CommandLine.InputPattern as InputPattern
import qualified Unison.HashQualified as HQ
import qualified Unison.HashQualified' as HQ'
import Unison.Names (Names)
import Unison.Prelude
import qualified Unison.Server.SearchResult as SR
import qualified Unison.Util.ColorText as CT
import qualified Unison.Util.Find as Find
import qualified Unison.Util.Pretty as P
import Unison.Util.TQueue (TQueue)
import qualified Unison.Util.TQueue as Q
@ -125,95 +106,6 @@ emojiNote lead s = P.group (fromString lead) <> "\n" <> P.wrap s
nothingTodo :: (ListLike s Char, IsString s) => P.Pretty s -> P.Pretty s
nothingTodo = emojiNote "😶"
completion :: String -> Line.Completion
completion s = Line.Completion s s True
completion' :: String -> Line.Completion
completion' s = Line.Completion s s False
-- discards formatting in favor of better alignment
-- prettyCompletion (s, p) = Line.Completion s (P.toPlainUnbroken p) True
-- preserves formatting, but Haskeline doesn't know how to align
prettyCompletion :: Bool -> (String, P.Pretty P.ColorText) -> Line.Completion
prettyCompletion endWithSpace (s, p) = Line.Completion s (P.toAnsiUnbroken p) endWithSpace
-- | Renders a completion option with the prefix matching the query greyed out.
prettyCompletionWithQueryPrefix ::
Bool ->
-- | query
String ->
-- | completion
String ->
Line.Completion
prettyCompletionWithQueryPrefix endWithSpace query s =
let coloredMatch = P.hiBlack (P.string query) <> P.string (drop (length query) s)
in Line.Completion s (P.toAnsiUnbroken coloredMatch) endWithSpace
fuzzyCompleteHashQualified :: Names -> String -> [Line.Completion]
fuzzyCompleteHashQualified b q0@(HQ'.fromString -> query) = case query of
Nothing -> []
Just query ->
fixupCompletion q0 $
makeCompletion <$> Find.fuzzyFindInBranch b query
where
makeCompletion (sr, p) =
prettyCompletion False (HQ.toString . SR.name $ sr, p)
fuzzyComplete :: String -> [String] -> [Line.Completion]
fuzzyComplete absQuery@('.' : _) ss = completeWithinQueryNamespace absQuery ss
fuzzyComplete fuzzyQuery ss =
fixupCompletion fuzzyQuery (prettyCompletion False <$> Find.simpleFuzzyFinder fuzzyQuery ss id)
-- | Constructs a list of 'Completion's from a query and completion options by
-- filtering them for prefix matches. A completion will be selected if it's an exact match for
-- a provided option.
exactComplete :: String -> [String] -> [Line.Completion]
exactComplete q ss = go <$> filter (isPrefixOf q) ss
where
go s = prettyCompletionWithQueryPrefix (s == q) q s
-- | Completes a list of options, limiting options to the same namespace as the query,
-- or the namespace's children if the query is itself a namespace.
--
-- E.g.
-- query: "base"
-- would match: ["base", "base.List", "base2"]
-- wouldn't match: ["base.List.map", "contrib", "base2.List"]
completeWithinQueryNamespace :: String -> [String] -> [Line.Completion]
completeWithinQueryNamespace q ss = (go <$> (limitToQueryNamespace q $ ss))
where
go s = prettyCompletionWithQueryPrefix (s == q) q s
limitToQueryNamespace :: String -> [String] -> [String]
limitToQueryNamespace query xs =
nubOrd $ catMaybes (fmap ((query <>) . thing) . List.stripPrefix query <$> xs)
where
thing ('.' : rest) = '.' : takeWhile (/= '.') rest
thing other = takeWhile (/= '.') other
prefixIncomplete :: String -> [String] -> [Line.Completion]
prefixIncomplete q ss = go <$> filter (isPrefixOf q) ss
where
go s =
prettyCompletion
False
(s, P.hiBlack (P.string q) <> P.string (drop (length q) s))
-- workaround for https://github.com/judah/haskeline/issues/100
-- if the common prefix of all the completions is smaller than
-- the query, we make all the replacements equal to the query,
-- which will preserve what the user has typed
fixupCompletion :: String -> [Line.Completion] -> [Line.Completion]
fixupCompletion _q [] = []
fixupCompletion _q [c] = [c]
fixupCompletion q cs@(h : t) =
let commonPrefix (h1 : t1) (h2 : t2) | h1 == h2 = h1 : commonPrefix t1 t2
commonPrefix _ _ = ""
overallCommonPrefix =
foldl commonPrefix (Line.replacement h) (Line.replacement <$> t)
in if not (q `isPrefixOf` overallCommonPrefix)
then [c {Line.replacement = q} | c <- cs]
else cs
parseInput ::
-- | Root branch, used to expand globs
Branch0 m ->

View File

@ -0,0 +1,353 @@
{-
This module defines tab-completion strategies for entering info via the CLI
-}
module Unison.CommandLine.Completion
( -- * Completers
exactComplete,
prefixCompleteTermOrType,
prefixCompleteTerm,
prefixCompleteType,
prefixCompletePatch,
noCompletions,
prefixCompleteNamespace,
-- Unused for now, but may be useful later
prettyCompletion,
fixupCompletion,
haskelineTabComplete,
)
where
import Control.Lens (ifoldMap)
import qualified Control.Lens as Lens
import Control.Lens.Cons (unsnoc)
import Data.List (isPrefixOf)
import qualified Data.List as List
import Data.List.Extra (nubOrdOn)
import qualified Data.List.NonEmpty as NE
import qualified Data.Map as Map
import Data.Set.NonEmpty (NESet)
import qualified Data.Set.NonEmpty as NESet
import qualified Data.Text as Text
import qualified System.Console.Haskeline as Line
import System.Console.Haskeline.Completion (Completion)
import qualified System.Console.Haskeline.Completion as Haskeline
import qualified U.Codebase.Branch as V2Branch
import qualified U.Codebase.Causal as V2Causal
import qualified U.Codebase.Reference as Reference
import qualified U.Codebase.Referent as Referent
import qualified U.Util.Monoid as Monoid
import Unison.Codebase (Codebase)
import qualified Unison.Codebase as Codebase
import qualified Unison.Codebase.Path as Path
import qualified Unison.Codebase.SqliteCodebase.Conversions as Cv
import qualified Unison.CommandLine.InputPattern as IP
import qualified Unison.Hash as H
import qualified Unison.HashQualified' as HQ'
import Unison.NameSegment (NameSegment (NameSegment))
import qualified Unison.NameSegment as NameSegment
import Unison.Prelude
import qualified Unison.ShortHash as SH
import qualified Unison.Util.Pretty as P
import Prelude hiding (readFile, writeFile)
-- | A completion func for use with Haskeline
haskelineTabComplete ::
Monad m =>
Map String IP.InputPattern ->
Codebase m v a ->
Path.Absolute ->
Line.CompletionFunc m
haskelineTabComplete patterns codebase currentPath = Line.completeWordWithPrev Nothing " " $ \prev word ->
-- User hasn't finished a command name, complete from command names
if null prev
then pure . exactComplete word $ Map.keys patterns
else -- User has finished a command name; use completions for that command
case words $ reverse prev of
h : t -> fromMaybe (pure []) $ do
p <- Map.lookup h patterns
argType <- IP.argType p (length t)
pure $ IP.suggestions argType word codebase currentPath
_ -> pure []
-- | Things which we may want to complete for.
data CompletionType
= NamespaceCompletion
| TermCompletion
| TypeCompletion
| PatchCompletion
deriving (Show, Eq, Ord)
-- | The empty completor.
noCompletions ::
Monad m =>
String ->
Codebase m v a ->
Path.Absolute ->
m [System.Console.Haskeline.Completion.Completion]
noCompletions _ _ _ = pure []
-- | Finds names of the selected completion types within the path provided by the query.
--
-- Given a codebase with these terms:
--
-- @@
-- .base.List.map.doc
-- .base.List
-- .bar.foo
-- @@
--
-- We will return:
--
-- @@
-- .> cd bas<Tab>
-- base
--
-- .> cd base<Tab>
-- base
-- base.List
--
-- .> cd base.<Tab>
-- base.List
--
-- .> cd base.List.<Tab>
-- base.List.map
--
-- If conflicted, or if there's a # in the query, we expand completions into short-hashes.
-- This is also a convenient way to just see the shorthash for a given term.
--
-- .> view base.List.map#<Tab>
-- base.List.map#0q926sgnn6
completeWithinNamespace ::
forall m v a.
Monad m =>
-- | The types of completions to return
NESet CompletionType ->
-- | The portion of this are that the user has already typed.
String ->
Codebase m v a ->
Path.Absolute ->
m [System.Console.Haskeline.Completion.Completion]
completeWithinNamespace compTypes query codebase currentPath = do
shortHashLen <- Codebase.hashLength codebase
Codebase.getShallowBranchFromRoot codebase absQueryPath >>= \case
Nothing -> do
pure []
Just cb -> do
b <- V2Causal.value cb
let currentBranchSuggestions =
namesInBranch shortHashLen b
& fmap (\(isFinished, match) -> (isFinished, Text.unpack . Path.toText' $ queryPathPrefix Lens.:> NameSegment.NameSegment match))
& filter (\(_isFinished, match) -> List.isPrefixOf query match)
& fmap (\(isFinished, match) -> prettyCompletionWithQueryPrefix isFinished query match)
childSuggestions <- getChildSuggestions shortHashLen b
pure . nubOrdOn Haskeline.replacement . List.sortOn Haskeline.replacement $ currentBranchSuggestions <> childSuggestions
where
queryPathPrefix :: Path.Path'
querySuffix :: NameSegment.NameSegment
(queryPathPrefix, querySuffix) = parseLaxPath'Query (Text.pack query)
absQueryPath :: Path.Absolute
absQueryPath = Path.resolve currentPath queryPathPrefix
getChildSuggestions :: Int -> V2Branch.Branch m -> m [Completion]
getChildSuggestions shortHashLen b = do
case querySuffix of
"" -> pure []
suffix -> do
case Map.lookup (Cv.namesegment1to2 suffix) (V2Branch.children b) of
Nothing -> pure []
Just childCausal -> do
childBranch <- V2Causal.value childCausal
namesInBranch shortHashLen childBranch
& fmap
( \(isFinished, match) -> (isFinished, Text.unpack . Path.toText' $ queryPathPrefix Lens.:> suffix Lens.:> NameSegment.NameSegment match)
)
& filter (\(_isFinished, match) -> List.isPrefixOf query match)
& fmap (\(isFinished, match) -> prettyCompletionWithQueryPrefix isFinished query match)
& pure
namesInBranch :: Int -> V2Branch.Branch m -> [(Bool, Text)]
namesInBranch hashLen b =
let textifyHQ :: (V2Branch.NameSegment -> r -> HQ'.HashQualified V2Branch.NameSegment) -> Map V2Branch.NameSegment (Map r metadata) -> [(Bool, Text)]
textifyHQ f xs =
xs
& hashQualifyCompletions f
& fmap (HQ'.toTextWith V2Branch.unNameSegment)
& fmap (True,)
in ((False,) <$> dotifyNamespaces (fmap V2Branch.unNameSegment . Map.keys $ V2Branch.children b))
<> Monoid.whenM (NESet.member TermCompletion compTypes) (textifyHQ (hqFromNamedV2Referent hashLen) $ V2Branch.terms b)
<> Monoid.whenM (NESet.member TypeCompletion compTypes) (textifyHQ (hqFromNamedV2Reference hashLen) $ V2Branch.types b)
<> Monoid.whenM (NESet.member PatchCompletion compTypes) (fmap ((True,) . V2Branch.unNameSegment) . Map.keys $ V2Branch.patches b)
-- Regrettably there'shqFromNamedV2Referencenot a great spot to combinators for V2 references and shorthashes right now.
hqFromNamedV2Referent :: Int -> V2Branch.NameSegment -> Referent.Referent -> HQ'.HashQualified V2Branch.NameSegment
hqFromNamedV2Referent hashLen n r = HQ'.HashQualified n (SH.take hashLen $ v2ReferentToShortHash r)
hqFromNamedV2Reference :: Int -> V2Branch.NameSegment -> Reference.Reference -> HQ'.HashQualified V2Branch.NameSegment
hqFromNamedV2Reference hashLen n r = HQ'.HashQualified n (SH.take hashLen $ v2ReferenceToShortHash r)
v2ReferentToShortHash :: Referent.Referent -> SH.ShortHash
v2ReferentToShortHash = \case
Referent.Ref r -> v2ReferenceToShortHash r
Referent.Con r conId ->
case v2ReferenceToShortHash r of
SH.ShortHash h p _con -> SH.ShortHash h p (Just $ tShow conId)
sh@(SH.Builtin {}) -> sh
v2ReferenceToShortHash :: Reference.Reference -> SH.ShortHash
v2ReferenceToShortHash (Reference.ReferenceBuiltin b) = SH.Builtin b
v2ReferenceToShortHash (Reference.ReferenceDerived (Reference.Id h i)) = SH.ShortHash (H.base32Hex h) (showComponentPos i) Nothing
showComponentPos :: Reference.Pos -> Maybe Text
showComponentPos 0 = Nothing
showComponentPos n = Just (tShow n)
hashQualifyCompletions :: forall r metadata. (V2Branch.NameSegment -> r -> HQ'.HashQualified V2Branch.NameSegment) -> Map V2Branch.NameSegment (Map r metadata) -> [HQ'.HashQualified V2Branch.NameSegment]
hashQualifyCompletions qualify defs = ifoldMap qualifyRefs defs
where
-- Qualify any conflicted definitions. If the query has a "#" in it, then qualify ALL
-- completions.
qualifyRefs :: V2Branch.NameSegment -> (Map r metadata) -> [HQ'.HashQualified V2Branch.NameSegment]
qualifyRefs n refs
| ((Text.isInfixOf "#" . NameSegment.toText) querySuffix) || length refs > 1 =
refs
& Map.keys
<&> qualify n
| otherwise = [HQ'.NameOnly n]
-- If we're not completing namespaces, then all namespace completions should automatically
-- drill-down by adding a trailing '.'
dotifyNamespaces :: [Text] -> [Text]
dotifyNamespaces namespaces =
if not (NESet.member NamespaceCompletion compTypes)
then fmap (<> ".") namespaces
else namespaces
-- | A path parser which which is more lax with respect to well formed paths,
-- specifically we can determine a valid path prefix with a (possibly empty) suffix query.
-- This is used in tab-completion where the difference between `.base` and `.base.` is
-- relevant, but can't be detected when running something like 'Path.fromText''
--
-- >>> parseLaxPath'Query ".base."
-- (.base,"")
--
-- >>> parseLaxPath'Query ".base"
-- (.,"base")
--
-- >>> parseLaxPath'Query ".base.List"
-- (.base,"List")
--
-- >>> parseLaxPath'Query ""
-- (,"")
--
-- >>> parseLaxPath'Query "base"
-- (,"base")
--
-- >>> parseLaxPath'Query "base."
-- (base,"")
--
-- >>> parseLaxPath'Query "base.List"
-- (base,"List")
parseLaxPath'Query :: Text -> (Path.Path', NameSegment)
parseLaxPath'Query txt =
case unsnoc (Text.splitOn "." txt) of
-- This case is impossible due to the behaviour of 'splitOn'
Nothing ->
(Path.relativeEmpty', NameSegment "")
-- ".base."
-- ".base.List"
Just ("" : pathPrefix, querySegment) -> (Path.AbsolutePath' . Path.Absolute . Path.fromList . fmap NameSegment $ pathPrefix, NameSegment querySegment)
-- ""
-- "base"
-- "base.List"
Just (pathPrefix, querySegment) ->
( Path.RelativePath' . Path.Relative . Path.fromList . fmap NameSegment $ pathPrefix,
NameSegment querySegment
)
-- | Completes a namespace argument by prefix-matching against the query.
prefixCompleteNamespace ::
forall m v a.
Monad m =>
String ->
Codebase m v a ->
Path.Absolute -> -- Current path
m [Line.Completion]
prefixCompleteNamespace = completeWithinNamespace (NESet.singleton NamespaceCompletion)
-- | Completes a term or type argument by prefix-matching against the query.
prefixCompleteTermOrType ::
forall m v a.
Monad m =>
String ->
Codebase m v a ->
Path.Absolute -> -- Current path
m [Line.Completion]
prefixCompleteTermOrType = completeWithinNamespace (NESet.fromList (TermCompletion NE.:| [TypeCompletion]))
-- | Completes a term argument by prefix-matching against the query.
prefixCompleteTerm ::
forall m v a.
Monad m =>
String ->
Codebase m v a ->
Path.Absolute -> -- Current path
m [Line.Completion]
prefixCompleteTerm = completeWithinNamespace (NESet.singleton TermCompletion)
-- | Completes a term or type argument by prefix-matching against the query.
prefixCompleteType ::
forall m v a.
Monad m =>
String ->
Codebase m v a ->
Path.Absolute -> -- Current path
m [Line.Completion]
prefixCompleteType = completeWithinNamespace (NESet.singleton TypeCompletion)
-- | Completes a patch argument by prefix-matching against the query.
prefixCompletePatch ::
forall m v a.
Monad m =>
String ->
Codebase m v a ->
Path.Absolute -> -- Current path
m [Line.Completion]
prefixCompletePatch = completeWithinNamespace (NESet.singleton PatchCompletion)
-- | Renders a completion option with the prefix matching the query greyed out.
prettyCompletionWithQueryPrefix ::
Bool ->
-- | query
String ->
-- | completion
String ->
Line.Completion
prettyCompletionWithQueryPrefix endWithSpace query s =
let coloredMatch = P.hiBlack (P.string query) <> P.string (drop (length query) s)
in Line.Completion s (P.toAnsiUnbroken coloredMatch) endWithSpace
-- discards formatting in favor of better alignment
-- prettyCompletion (s, p) = Line.Completion s (P.toPlainUnbroken p) True
-- preserves formatting, but Haskeline doesn't know how to align
prettyCompletion :: Bool -> (String, P.Pretty P.ColorText) -> Line.Completion
prettyCompletion endWithSpace (s, p) = Line.Completion s (P.toAnsiUnbroken p) endWithSpace
-- | Constructs a list of 'Completion's from a query and completion options by
-- filtering them for prefix matches. A completion will be selected if it's an exact match for
-- a provided option.
exactComplete :: String -> [String] -> [Line.Completion]
exactComplete q ss = go <$> filter (isPrefixOf q) ss
where
go s = prettyCompletionWithQueryPrefix (s == q) q s
-- workaround for https://github.com/judah/haskeline/issues/100
-- if the common prefix of all the completions is smaller than
-- the query, we make all the replacements equal to the query,
-- which will preserve what the user has typed
fixupCompletion :: String -> [Line.Completion] -> [Line.Completion]
fixupCompletion _q [] = []
fixupCompletion _q [c] = [c]
fixupCompletion q cs@(h : t) =
let commonPrefix (h1 : t1) (h2 : t2) | h1 == h2 = h1 : commonPrefix t1 t2
commonPrefix _ _ = ""
overallCommonPrefix =
foldl commonPrefix (Line.replacement h) (Line.replacement <$> t)
in if not (q `isPrefixOf` overallCommonPrefix)
then [c {Line.replacement = q} | c <- cs]
else cs

View File

@ -2,12 +2,22 @@
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ViewPatterns #-}
module Unison.CommandLine.InputPattern where
module Unison.CommandLine.InputPattern
( InputPattern (..),
ArgumentType (..),
argType,
IsOptional (..),
Visibility (..),
-- * Currently Unused
minArgs,
maxArgs,
)
where
import Data.Set (Set)
import qualified System.Console.Haskeline as Line
import Unison.Codebase (Codebase)
import Unison.Codebase.Branch (Branch)
import Unison.Codebase.Editor.Input (Input (..))
import Unison.Codebase.Path as Path
import qualified Unison.CommandLine.Globbing as Globbing
@ -43,7 +53,6 @@ data ArgumentType = ArgumentType
Monad m =>
String ->
Codebase m v a ->
Branch m -> -- Root Branch
Path.Absolute -> -- Current path
m [Line.Completion],
-- | Select which targets glob patterns may expand into for this argument.
@ -105,12 +114,3 @@ maxArgs ip@(fmap fst . argTypes -> args) = go args
<> show (patternName ip)
<> "): "
<> show args
noSuggestions ::
Monad m =>
String ->
Codebase m v a ->
Branch m ->
Path.Absolute ->
m [Line.Completion]
noSuggestions _ _ _ _ = pure []

View File

@ -5,8 +5,7 @@ module Unison.CommandLine.InputPatterns where
import qualified Control.Lens.Cons as Cons
import Data.Bifunctor (Bifunctor (bimap), first)
import Data.List (intercalate, isPrefixOf)
import Data.List.Extra (nubOrdOn)
import Data.List (intercalate)
import qualified Data.List.NonEmpty as NE
import qualified Data.Map as Map
import Data.Proxy (Proxy (..))
@ -14,12 +13,9 @@ import qualified Data.Set as Set
import qualified Data.Text as Text
import Data.Void (Void)
import System.Console.Haskeline.Completion (Completion (Completion))
import qualified System.Console.Haskeline.Completion as Completion
import qualified Text.Megaparsec as P
import Unison.Codebase (Codebase)
import qualified Unison.Codebase.Branch as Branch
import qualified Unison.Codebase.Branch.Merge as Branch
import qualified Unison.Codebase.Branch.Names as Branch
import Unison.Codebase.Editor.Input (Input)
import qualified Unison.Codebase.Editor.Input as Input
import Unison.Codebase.Editor.Output.PushPull (PushPull (Pull, Push))
@ -34,6 +30,7 @@ import qualified Unison.Codebase.SyncMode as SyncMode
import Unison.Codebase.Verbosity (Verbosity)
import qualified Unison.Codebase.Verbosity as Verbosity
import Unison.CommandLine
import Unison.CommandLine.Completion
import qualified Unison.CommandLine.Globbing as Globbing
import Unison.CommandLine.InputPattern
( ArgumentType (..),
@ -44,13 +41,11 @@ import qualified Unison.CommandLine.InputPattern as I
import qualified Unison.HashQualified as HQ
import Unison.Name (Name)
import qualified Unison.Name as Name
import Unison.NameSegment (NameSegment (NameSegment))
import qualified Unison.Names as Names
import qualified Unison.NameSegment as NameSegment
import Unison.Prelude
import qualified Unison.Util.ColorText as CT
import Unison.Util.Monoid (intercalateMap)
import qualified Unison.Util.Pretty as P
import qualified Unison.Util.Relation as R
showPatternHelp :: InputPattern -> P.Pretty CT.ColorText
showPatternHelp i =
@ -155,7 +150,7 @@ load =
"load"
[]
I.Visible
[(Optional, noCompletions)]
[(Optional, noCompletionsArg)]
( P.wrapColumn2
[ ( makeExample' load,
"parses, typechecks, and evaluates the most recent scratch file."
@ -177,7 +172,7 @@ add =
"add"
[]
I.Visible
[(ZeroPlus, noCompletions)]
[(ZeroPlus, noCompletionsArg)]
( "`add` adds to the codebase all the definitions from the most recently "
<> "typechecked file."
)
@ -189,7 +184,7 @@ previewAdd =
"add.preview"
[]
I.Visible
[(ZeroPlus, noCompletions)]
[(ZeroPlus, noCompletionsArg)]
( "`add.preview` previews additions to the codebase from the most recently "
<> "typechecked file. This command only displays cached typechecking "
<> "results. Use `load` to reparse & typecheck the file if the context "
@ -203,7 +198,7 @@ updateNoPatch =
"update.nopatch"
["un"]
I.Visible
[(ZeroPlus, noCompletions)]
[(ZeroPlus, noCompletionsArg)]
( P.wrap
( makeExample' updateNoPatch
<> "works like"
@ -237,7 +232,7 @@ update =
"update"
[]
I.Visible
[(Optional, patchArg), (ZeroPlus, noCompletions)]
[(Optional, patchArg), (ZeroPlus, noCompletionsArg)]
( P.wrap
( makeExample' update
<> "works like"
@ -281,7 +276,7 @@ previewUpdate =
"update.preview"
[]
I.Visible
[(ZeroPlus, noCompletions)]
[(ZeroPlus, noCompletionsArg)]
( "`update.preview` previews updates to the codebase from the most "
<> "recently typechecked file. This command only displays cached "
<> "typechecking results. Use `load` to reparse & typecheck the file if "
@ -366,7 +361,7 @@ displayTo =
"display.to"
[]
I.Visible
[(Required, noCompletions), (ZeroPlus, definitionQueryArg)]
[(Required, noCompletionsArg), (ZeroPlus, definitionQueryArg)]
( P.wrap $
makeExample displayTo ["<filename>", "foo"]
<> "prints a rendered version of the term `foo` to the given file."
@ -448,7 +443,7 @@ find' cmd fscope =
cmd
[]
I.Visible
[(ZeroPlus, fuzzyDefinitionQueryArg)]
[(ZeroPlus, exactDefinitionArg)]
( P.wrapColumn2
[ ("`find`", "lists all definitions in the current namespace."),
( "`find foo`",
@ -497,7 +492,7 @@ findVerbose =
"find.verbose"
[]
I.Visible
[(ZeroPlus, fuzzyDefinitionQueryArg)]
[(ZeroPlus, exactDefinitionArg)]
( "`find.verbose` searches for definitions like `find`, but includes hashes "
<> "and aliases in the results."
)
@ -509,7 +504,7 @@ findVerboseAll =
"find.all.verbose"
[]
I.Visible
[(ZeroPlus, fuzzyDefinitionQueryArg)]
[(ZeroPlus, exactDefinitionArg)]
( "`find.all.verbose` searches for definitions like `find.all`, but includes hashes "
<> "and aliases in the results."
)
@ -738,7 +733,7 @@ aliasMany =
"alias.many"
["copy"]
I.Visible
[(Required, definitionQueryArg), (OnePlus, exactDefinitionOrPathArg)]
[(Required, definitionQueryArg), (OnePlus, exactDefinitionArg)]
( P.group . P.lines $
[ P.wrap $
P.group (makeExample aliasMany ["<relative1>", "[relative2...]", "<namespace>"])
@ -1087,6 +1082,22 @@ pullExhaustive =
_ -> Left (I.help pull)
)
debugTabCompletion :: InputPattern
debugTabCompletion =
InputPattern
"debug.tab-complete"
[]
I.Hidden
[(ZeroPlus, noCompletionsArg)]
( P.lines
[ P.wrap $ "This command can be used to test and debug ucm's tab-completion within transcripts.",
P.wrap $ "Completions which are finished are prefixed with a *"
]
)
( \inputs ->
Right $ Input.DebugTabCompletionI inputs
)
push :: InputPattern
push =
InputPattern
@ -1567,7 +1578,7 @@ topicNameArg :: ArgumentType
topicNameArg =
ArgumentType
{ typeName = "topic",
suggestions = \q _ _ _ -> pure (exactComplete q $ Map.keys helpTopicsMap),
suggestions = \q _ _ -> pure (exactComplete q $ Map.keys helpTopicsMap),
globTargets = mempty
}
@ -1575,7 +1586,7 @@ codebaseServerNameArg :: ArgumentType
codebaseServerNameArg =
ArgumentType
{ typeName = "codebase-server",
suggestions = \q _ _ _ -> pure (exactComplete q $ Map.keys helpTopicsMap),
suggestions = \q _ _ -> pure (exactComplete q $ Map.keys helpTopicsMap),
globTargets = mempty
}
@ -1949,7 +1960,7 @@ debugDumpNamespace =
"debug.dump-namespace"
[]
I.Visible
[(Required, noCompletions)]
[(Required, noCompletionsArg)]
"Dump the namespace to a text file"
(const $ Right Input.DebugDumpNamespacesI)
@ -1959,7 +1970,7 @@ debugDumpNamespaceSimple =
"debug.dump-namespace-simple"
[]
I.Visible
[(Required, noCompletions)]
[(Required, noCompletionsArg)]
"Dump the namespace to a text file"
(const $ Right Input.DebugDumpNamespaceSimpleI)
@ -1969,7 +1980,7 @@ debugClearWatchCache =
"debug.clear-cache"
[]
I.Visible
[(Required, noCompletions)]
[(Required, noCompletionsArg)]
"Clear the watch expression cache"
(const $ Right Input.DebugClearWatchI)
@ -2050,7 +2061,7 @@ execute =
"run"
[]
I.Visible
[(Required, exactDefinitionTermQueryArg), (ZeroPlus, noCompletions)]
[(Required, exactDefinitionTermQueryArg), (ZeroPlus, noCompletionsArg)]
( P.wrapColumn2
[ ( "`run mymain args...`",
"Runs `!mymain`, where `mymain` is searched for in the most recent"
@ -2091,7 +2102,7 @@ makeStandalone =
"compile"
["compile.output"]
I.Visible
[(Required, exactDefinitionTermQueryArg), (Required, noCompletions)]
[(Required, exactDefinitionTermQueryArg), (Required, noCompletionsArg)]
( P.wrapColumn2
[ ( "`compile main file`",
"Outputs a stand alone file that can be directly loaded and"
@ -2112,7 +2123,7 @@ createAuthor =
"create.author"
[]
I.Visible
[(Required, noCompletions), (Required, noCompletions)]
[(Required, noCompletionsArg), (Required, noCompletionsArg)]
( makeExample createAuthor ["alicecoder", "\"Alice McGee\""]
<> "creates"
<> backtick "alicecoder"
@ -2287,11 +2298,19 @@ validInputs =
debugDumpNamespaceSimple,
debugClearWatchCache,
debugDoctor,
debugTabCompletion,
gist,
authLogin,
printVersion
]
-- | A map of all command patterns by pattern name or alias.
patternMap :: Map String InputPattern
patternMap =
Map.fromList $
validInputs
>>= (\p -> (I.patternName p, p) : ((,p) <$> I.aliases p))
visibleInputs :: [InputPattern]
visibleInputs = filter ((== I.Visible) . I.visibility) validInputs
@ -2302,43 +2321,34 @@ commandNameArg :: ArgumentType
commandNameArg =
ArgumentType
{ typeName = "command",
suggestions = \q _ _ _ -> pure (exactComplete q (commandNames <> Map.keys helpTopicsMap)),
suggestions = \q _ _ -> pure (exactComplete q (commandNames <> Map.keys helpTopicsMap)),
globTargets = mempty
}
exactDefinitionOrPathArg :: ArgumentType
exactDefinitionOrPathArg =
exactDefinitionArg :: ArgumentType
exactDefinitionArg =
ArgumentType
{ typeName = "definition or path",
suggestions =
allCompletors
[ termCompletor exactComplete,
typeCompletor exactComplete,
pathCompletor exactComplete (Set.map Path.toText . Branch.deepPaths)
],
globTargets = Set.fromList [Globbing.Term, Globbing.Type, Globbing.Namespace]
{ typeName = "definition",
suggestions = prefixCompleteTermOrType,
globTargets = Set.fromList [Globbing.Term, Globbing.Type]
}
-- todo: improve this
fuzzyDefinitionQueryArg :: ArgumentType
fuzzyDefinitionQueryArg =
ArgumentType
{ typeName = "fuzzy definition query",
suggestions =
bothCompletors
(termCompletor fuzzyComplete)
(typeCompletor fuzzyComplete),
suggestions = prefixCompleteTermOrType,
globTargets = Set.fromList [Globbing.Term, Globbing.Type]
}
definitionQueryArg :: ArgumentType
definitionQueryArg = fuzzyDefinitionQueryArg {typeName = "definition query"}
definitionQueryArg = exactDefinitionArg {typeName = "definition query"}
exactDefinitionTypeQueryArg :: ArgumentType
exactDefinitionTypeQueryArg =
ArgumentType
{ typeName = "type definition query",
suggestions = typeCompletor exactComplete,
suggestions = prefixCompleteType,
globTargets = Set.fromList [Globbing.Type]
}
@ -2346,123 +2356,44 @@ exactDefinitionTermQueryArg :: ArgumentType
exactDefinitionTermQueryArg =
ArgumentType
{ typeName = "term definition query",
suggestions = termCompletor exactComplete,
suggestions = prefixCompleteTerm,
globTargets = Set.fromList [Globbing.Term]
}
typeCompletor ::
Applicative m =>
(String -> [String] -> [Completion]) ->
String ->
Codebase m v a ->
Branch.Branch m ->
Path.Absolute ->
m [Completion]
typeCompletor filterQuery = pathCompletor filterQuery go
where
go = Set.map HQ.toText . R.dom . Names.hashQualifyTypesRelation . Names.types . Branch.toNames
termCompletor ::
Applicative m =>
(String -> [String] -> [Completion]) ->
String ->
Codebase m v a ->
Branch.Branch m ->
Path.Absolute ->
m [Completion]
termCompletor filterQuery = pathCompletor filterQuery go
where
go = Set.map HQ.toText . R.dom . Names.hashQualifyTermsRelation . Names.terms . Branch.toNames
patchArg :: ArgumentType
patchArg =
ArgumentType
{ typeName = "patch",
suggestions =
pathCompletor
exactComplete
(Set.map Name.toText . Map.keysSet . Branch.deepEdits),
suggestions = prefixCompletePatch,
globTargets = Set.fromList []
}
allCompletors ::
Monad m =>
( [String -> Codebase m v a -> Branch.Branch m -> Path.Absolute -> m [Completion]] ->
(String -> Codebase m v a -> Branch.Branch m -> Path.Absolute -> m [Completion])
)
allCompletors = foldl' bothCompletors I.noSuggestions
bothCompletors ::
(Monad m) =>
(String -> t2 -> t3 -> t4 -> m [Completion]) ->
(String -> t2 -> t3 -> t4 -> m [Completion]) ->
String ->
t2 ->
t3 ->
t4 ->
m [Completion]
bothCompletors c1 c2 q code b currentPath = do
suggestions1 <- c1 q code b currentPath
suggestions2 <- c2 q code b currentPath
pure . fixupCompletion q
. nubOrdOn Completion.display
$ suggestions1 ++ suggestions2
-- | A completer for namespace paths.
pathCompletor ::
Applicative f =>
-- | Turns a query and list of possible completions into a 'Completion'.
(String -> [String] -> [Completion]) ->
-- | Construct completions given ucm's current branch context, or the root namespace if
-- the query is absolute.
(Branch.Branch0 m -> Set Text) ->
-- | The portion of this arg that the user has already typed.
String ->
codebase ->
Branch.Branch m ->
Path.Absolute ->
f [Completion]
pathCompletor filterQuery getNames query _code b p =
let b0root = Branch.head b
b0local = Branch.getAt0 (Path.unabsolute p) b0root
in -- todo: if these sets are huge, maybe trim results
pure . filterQuery query . map Text.unpack $
toList (getNames b0local)
++ if "." `isPrefixOf` query
then map ("." <>) (toList (getNames b0root))
else []
namespaceArg :: ArgumentType
namespaceArg =
ArgumentType
{ typeName = "namespace",
suggestions = pathCompletor completeWithinQueryNamespace (Set.fromList . allSubNamespaces),
suggestions = prefixCompleteNamespace,
globTargets = Set.fromList [Globbing.Namespace]
}
-- | Recursively collects all names of namespaces which are children of the branch.
allSubNamespaces :: Branch.Branch0 m -> [Text]
allSubNamespaces b =
flip Map.foldMapWithKey (Branch.nonEmptyChildren b) $
\(NameSegment k) (Branch.head -> b') ->
(k : fmap (\sn -> k <> "." <> sn) (allSubNamespaces b'))
-- | Names of child branches of the branch, only gives options for one 'layer' deeper at a time.
childNamespaceNames :: Branch.Branch0 m -> [Text]
childNamespaceNames b = NameSegment.toText <$> Map.keys (Branch.nonEmptyChildren b)
newNameArg :: ArgumentType
newNameArg =
ArgumentType
{ typeName = "new-name",
suggestions =
pathCompletor
prefixIncomplete
(Set.map ((<> ".") . Path.toText) . Branch.deepPaths),
prefixCompleteNamespace,
globTargets = mempty
}
noCompletions :: ArgumentType
noCompletions =
noCompletionsArg :: ArgumentType
noCompletionsArg =
ArgumentType
{ typeName = "word",
suggestions = I.noSuggestions,
suggestions = noCompletions,
globTargets = mempty
}
@ -2473,7 +2404,7 @@ gitUrlArg =
{ typeName = "git-url",
suggestions =
let complete s = pure [Completion s s False]
in \input _ _ _ -> case input of
in \input _ _ -> case input of
"gh" -> complete "git(https://github.com/"
"gl" -> complete "git(https://gitlab.com/"
"bb" -> complete "git(https://bitbucket.com/"
@ -2491,7 +2422,7 @@ remoteNamespaceArg =
{ typeName = "remote-namespace",
suggestions =
let complete s = pure [Completion s s False]
in \input _ _ _ -> case input of
in \input _ _ -> case input of
"gh" -> complete "git(https://github.com/"
"gl" -> complete "git(https://gitlab.com/"
"bb" -> complete "git(https://bitbucket.com/"

View File

@ -12,7 +12,6 @@ import Control.Monad.Catch (MonadMask)
import qualified Crypto.Random as Random
import Data.Configurator.Types (Config)
import Data.IORef
import qualified Data.Map as Map
import qualified Data.Text as Text
import qualified Data.Text.Lazy.IO as Text.Lazy
import qualified System.Console.Haskeline as Line
@ -34,9 +33,8 @@ import Unison.Codebase.Editor.UCMVersion (UCMVersion)
import qualified Unison.Codebase.Path as Path
import qualified Unison.Codebase.Runtime as Runtime
import Unison.CommandLine
import Unison.CommandLine.InputPattern (ArgumentType (suggestions), InputPattern (aliases, patternName))
import qualified Unison.CommandLine.InputPattern as IP
import Unison.CommandLine.InputPatterns (validInputs)
import Unison.CommandLine.Completion (haskelineTabComplete)
import qualified Unison.CommandLine.InputPatterns as IP
import Unison.CommandLine.OutputMessages (notifyNumbered, notifyUser)
import qualified Unison.CommandLine.Welcome as Welcome
import Unison.Parser.Ann (Ann)
@ -52,13 +50,12 @@ import qualified UnliftIO
getUserInput ::
forall m v a.
(MonadIO m, MonadMask m) =>
Map String InputPattern ->
Codebase m v a ->
Branch m ->
Path.Absolute ->
[String] ->
m Input
getUserInput patterns codebase rootBranch currentPath numberedArgs =
getUserInput codebase rootBranch currentPath numberedArgs =
Line.runInputT
settings
(haskelineCtrlCHandling go)
@ -81,25 +78,14 @@ getUserInput patterns codebase rootBranch currentPath numberedArgs =
Just l -> case words l of
[] -> go
ws ->
case parseInput (Branch.head rootBranch) currentPath numberedArgs patterns $ ws of
case parseInput (Branch.head rootBranch) currentPath numberedArgs IP.patternMap $ ws of
Left msg -> do
liftIO $ putPrettyLn msg
go
Right i -> pure i
settings :: Line.Settings m
settings = Line.Settings tabComplete (Just ".unisonHistory") True
tabComplete :: Line.CompletionFunc m
tabComplete = Line.completeWordWithPrev Nothing " " $ \prev word ->
-- User hasn't finished a command name, complete from command names
if null prev
then pure . exactComplete word $ Map.keys patterns
else -- User has finished a command name; use completions for that command
case words $ reverse prev of
h : t -> fromMaybe (pure []) $ do
p <- Map.lookup h patterns
argType <- IP.argType p (length t)
pure $ suggestions argType word codebase rootBranch currentPath
_ -> pure []
tabComplete = haskelineTabComplete IP.patternMap codebase currentPath
main ::
FilePath ->
@ -120,15 +106,9 @@ main dir welcome initialPath (config, cancelConfig) initialInputs runtime sbRunt
initialInputsRef <- newIORef $ welcomeEvents ++ initialInputs
pageOutput <- newIORef True
cancelFileSystemWatch <- watchFileSystem eventQueue dir
let patternMap :: Map String InputPattern
patternMap =
Map.fromList $
validInputs
>>= (\p -> (patternName p, p) : ((,p) <$> aliases p))
let getInput :: Cli.LoopState -> IO Input
getInput loopState = do
getUserInput
patternMap
codebase
(loopState ^. #root)
(loopState ^. #currentPath)

View File

@ -28,6 +28,7 @@ import qualified Network.HTTP.Types as Http
import Network.URI (URI)
import qualified Network.URI.Encode as URI
import qualified Servant.Client as Servant
import qualified System.Console.Haskeline.Completion as Completion
import System.Directory
( canonicalizePath,
doesFileExist,
@ -646,8 +647,8 @@ notifyUser dir o = case o of
CachedTests 0 _ -> pure . P.callout "😶" $ "No tests to run."
CachedTests n n'
| n == n' ->
pure $
P.lines [cache, "", displayTestResults True ppe oks fails]
pure $
P.lines [cache, "", displayTestResults True ppe oks fails]
CachedTests _n m ->
pure $
if m == 0
@ -656,6 +657,7 @@ notifyUser dir o = case o of
P.indentN 2 $
P.lines ["", cache, "", displayTestResults False ppe oks fails, "", ""]
where
NewlyComputed -> do
clearCurrentLine
pure $
@ -1726,6 +1728,16 @@ notifyUser dir o = case o of
IntegrityCheck result -> pure $ case result of
NoIntegrityErrors -> "🎉 No issues detected 🎉"
IntegrityErrorDetected ns -> prettyPrintIntegrityErrors ns
DisplayDebugCompletions completions ->
pure $
P.column2
( completions <&> \comp ->
let isCompleteTxt =
if Completion.isFinished comp
then "*"
else ""
in (isCompleteTxt, P.string (Completion.replacement comp))
)
where
_nameChange _cmd _pastTenseCmd _oldName _newName _r = error "todo"
expectedEmptyPushDest writeRemotePath =
@ -2358,7 +2370,7 @@ showDiffNamespace ::
(Pretty, NumberedArgs)
showDiffNamespace _ _ _ _ diffOutput
| OBD.isEmpty diffOutput =
("The namespaces are identical.", mempty)
("The namespaces are identical.", mempty)
showDiffNamespace sn ppe oldPath newPath OBD.BranchDiffOutput {..} =
(P.sepNonEmpty "\n\n" p, toList args)
where

View File

@ -5,7 +5,6 @@ import System.Environment (getArgs)
import System.IO
import System.IO.CodePage (withCP65001)
import qualified Unison.Test.ClearCache as ClearCache
import qualified Unison.Test.CommandLine as CommandLine
import qualified Unison.Test.GitSync as GitSync
import qualified Unison.Test.UriParser as UriParser
import qualified Unison.Test.VersionParser as VersionParser
@ -14,7 +13,6 @@ test :: Test ()
test =
tests
[ ClearCache.test,
CommandLine.test,
GitSync.test,
UriParser.test,
VersionParser.test

View File

@ -1,40 +0,0 @@
{-# LANGUAGE RecordWildCards #-}
module Unison.Test.CommandLine where
import EasyTest
import qualified System.Console.Haskeline as Line
import Unison.CommandLine (completeWithinQueryNamespace)
data CompletionTest = CT {query :: String, expected :: [(String, Bool)], options :: [String]}
testCompletion :: (String -> [String] -> [Line.Completion]) -> CompletionTest -> Test ()
testCompletion compl CT {..} =
expectEqual expected ((\(Line.Completion {..}) -> (replacement, isFinished)) <$> compl query options)
test :: Test ()
test = scope "commandline" $ do
scope "completion" $ do
scope "completeWithinQueryNamespace" $ do
scope "only completes up to a single namespace boundary" $ do
testCompletion completeWithinQueryNamespace $
CT
{ query = ".ba",
expected = [(".base", False)],
options = [".base", ".base.List", ".base.Map"]
}
scope "completes into the next namespace if query is a complete namespace" $ do
testCompletion completeWithinQueryNamespace $
CT
{ query = ".base",
expected = [(".base", True), (".base.List", False), (".base.Map", False)],
options = [".base", ".base.List", ".base.Map"]
}
scope "completes " $ do
testCompletion completeWithinQueryNamespace $
CT
{ query = ".f",
expected = [(".function", False), (".facade", False), (".fellows", False)],
options = [".function", ".facade", ".fellows"]
}

View File

@ -51,6 +51,7 @@ library
Unison.Codebase.Editor.VersionParser
Unison.Codebase.TranscriptParser
Unison.CommandLine
Unison.CommandLine.Completion
Unison.CommandLine.DisplayValues
Unison.CommandLine.FuzzySelect
Unison.CommandLine.Globbing
@ -496,7 +497,6 @@ test-suite cli-tests
main-is: Main.hs
other-modules:
Unison.Test.ClearCache
Unison.Test.CommandLine
Unison.Test.GitSync
Unison.Test.Ucm
Unison.Test.UriParser

View File

@ -0,0 +1,48 @@
# Tab Completion
Test that tab completion works as expected.
## Tab Complete Command Names
```ucm
.> debug.tab-complete vi
.> debug.tab-complete delete.
```
## Tab complete terms & types
```unison
subnamespace.someName = 1
subnamespace.someOtherName = 2
othernamespace.someName = 3
unique type subnamespace.AType = A | B
```
```ucm:hide
.> add
```
```ucm
-- Should tab complete namespaces since they may contain terms/types
.> debug.tab-complete view sub
-- Should complete things from child namespaces of the current query
.> debug.tab-complete view subnamespace
.> debug.tab-complete view subnamespace.
-- Should prefix-filter by query suffix
.> debug.tab-complete view subnamespace.some
.> debug.tab-complete view subnamespace.someOther
-- Should tab complete absolute names
.othernamespace> debug.tab-complete view .subnamespace.some
```
## Tab complete namespaces
```ucm
-- Should tab complete namespaces
.> debug.tab-complete cd sub
.> debug.tab-complete cd subnamespace
.> debug.tab-complete cd subnamespace.
```

View File

@ -0,0 +1,107 @@
# Tab Completion
Test that tab completion works as expected.
## Tab Complete Command Names
```ucm
.> debug.tab-complete vi
view
view.patch
.> debug.tab-complete delete.
delete.link
delete.namespace
delete.namespace.force
delete.patch
delete.term
delete.term-replacement
delete.type
delete.type-replacement
```
## Tab complete terms & types
```unison
subnamespace.someName = 1
subnamespace.someOtherName = 2
othernamespace.someName = 3
unique type subnamespace.AType = A | B
```
```ucm
I found and typechecked these definitions in scratch.u. If you
do an `add` or `update`, here's how your codebase would
change:
⍟ These new definitions are ok to `add`:
unique type subnamespace.AType
othernamespace.someName : ##Nat
subnamespace.someName : ##Nat
subnamespace.someOtherName : ##Nat
```
```ucm
-- Should tab complete namespaces since they may contain terms/types
.> debug.tab-complete view sub
subnamespace.
-- Should complete things from child namespaces of the current query
.> debug.tab-complete view subnamespace
subnamespace.
* subnamespace.AType
subnamespace.AType.
* subnamespace.someName
* subnamespace.someOtherName
.> debug.tab-complete view subnamespace.
* subnamespace.AType
subnamespace.AType.
* subnamespace.someName
* subnamespace.someOtherName
-- Should prefix-filter by query suffix
.> debug.tab-complete view subnamespace.some
* subnamespace.someName
* subnamespace.someOtherName
.> debug.tab-complete view subnamespace.someOther
* subnamespace.someOtherName
-- Should tab complete absolute names
.othernamespace> debug.tab-complete view .subnamespace.some
* .subnamespace.someName
* .subnamespace.someOtherName
```
## Tab complete namespaces
```ucm
-- Should tab complete namespaces
.> debug.tab-complete cd sub
subnamespace
.> debug.tab-complete cd subnamespace
subnamespace
subnamespace.AType
.> debug.tab-complete cd subnamespace.
subnamespace.AType
```