Skip to content

Commit c42cbb2

Browse files
authored
Merge pull request #5776 from unisonweb/cp/file-completions
Add tab-completion and fzf completion for file arguments
2 parents 9a63fdd + 513625c commit c42cbb2

File tree

6 files changed

+122
-62
lines changed

6 files changed

+122
-62
lines changed

unison-cli/src/Unison/Codebase/Editor/HandleInput.hs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -632,7 +632,9 @@ loop e = do
632632
( \_ (paramName, IP.ParameterType {fzfResolver}) arg ->
633633
if arg == "_"
634634
then case fzfResolver of
635-
Just IP.FZFResolver {getOptions} -> do
635+
Just IP.DefaultFZFFileSearch -> do
636+
(,[]) <$> Cli.respond (DebugDisplayFuzzyOptions paramName ["<files>"])
637+
Just (IP.FetchOptions getOptions) -> do
636638
pp <- Cli.getCurrentProjectPath
637639
results <- liftIO $ getOptions codebase pp currentBranch
638640
(,[]) <$> Cli.respond (DebugDisplayFuzzyOptions paramName (Text.unpack <$> results))

unison-cli/src/Unison/CommandLine.hs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -239,12 +239,19 @@ fzfResolve codebase ppCtx getCurrentBranch InputPattern.Parameters {requiredPara
239239
fzfResolver
240240
fuzzyFillArg ::
241241
Bool -> Text -> InputPattern.FZFResolver -> MaybeT (ExceptT FZFResolveFailure IO) (NonEmpty InputPattern.Argument)
242-
fuzzyFillArg allowMulti argDesc InputPattern.FZFResolver {getOptions} = MaybeT do
242+
fuzzyFillArg allowMulti argDesc fzfResolver = MaybeT do
243243
currentBranch <- Branch.withoutTransitiveLibs <$> liftIO getCurrentBranch
244-
options <- liftIO $ getOptions codebase ppCtx currentBranch
245-
when (null options) . throwError $ NoFZFOptions argDesc
246-
liftIO $ PrettyTerm.putPrettyLn' (FZFResolvers.fuzzySelectHeader argDesc)
247-
results <- liftIO (Fuzzy.fuzzySelect Fuzzy.defaultOptions {Fuzzy.allowMultiSelect = allowMulti} id options)
244+
results <- case fzfResolver of
245+
InputPattern.FetchOptions getOptions -> do
246+
options <- liftIO $ getOptions codebase ppCtx currentBranch
247+
when (null options) . throwError $ NoFZFOptions argDesc
248+
liftIO $ PrettyTerm.putPrettyLn' (FZFResolvers.fuzzySelectHeader argDesc)
249+
let selections = Fuzzy.SelectFromChoices id options
250+
liftIO (Fuzzy.fuzzySelect Fuzzy.defaultOptions {Fuzzy.allowMultiSelect = allowMulti} selections)
251+
InputPattern.DefaultFZFFileSearch -> do
252+
liftIO $ PrettyTerm.putPrettyLn' (FZFResolvers.fuzzySelectHeader argDesc)
253+
let selections = Fuzzy.SelectFiles
254+
liftIO (Fuzzy.fuzzySelect Fuzzy.defaultOptions {Fuzzy.allowMultiSelect = allowMulti} selections)
248255
-- If the user triggered the fuzzy finder, but selected nothing, abort the command rather than continuing
249256
-- execution with no arguments.
250257
pure $ fmap (Left . Text.unpack <$>) . nonEmpty =<< results

unison-cli/src/Unison/CommandLine/Completion.hs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ module Unison.CommandLine.Completion
1616
fixupCompletion,
1717
haskelineTabComplete,
1818
sharePathCompletion,
19+
filenameCompletion,
1920
)
2021
where
2122

@@ -439,3 +440,13 @@ instance Aeson.FromJSON SearchResult where
439440
handle <- obj Aeson..: "handle"
440441
tag <- obj Aeson..: "tag"
441442
pure $ SearchResult {..}
443+
444+
filenameCompletion ::
445+
(MonadIO m) =>
446+
String ->
447+
m [Completion]
448+
filenameCompletion query = do
449+
-- Haskeline uses a zipper-style cursor format, so it expects the prefix to be reversed.
450+
let prefix = reverse query
451+
(_leftovers, results) <- Line.completeFilename (prefix, "")
452+
pure results

unison-cli/src/Unison/CommandLine/FZFResolvers.hs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ import Unison.Util.Relation qualified as Relation
5555

5656
type OptionFetcher = Codebase IO Symbol Ann -> PP.ProjectPath -> Branch0 IO -> IO [Text]
5757

58-
data FZFResolver = FZFResolver
59-
{ getOptions :: OptionFetcher
60-
}
58+
data FZFResolver
59+
= FetchOptions OptionFetcher
60+
| DefaultFZFFileSearch
6161

6262
instance Show FZFResolver where
6363
show _ = "<FZFResolver>"
@@ -126,33 +126,33 @@ projectDependencyOptions _codebase _projCtx searchBranch0 = do
126126
-- Returned Path's will match the provided 'Position' type.
127127
fuzzySelectFromList :: [Text] -> FZFResolver
128128
fuzzySelectFromList options =
129-
(FZFResolver {getOptions = \_codebase _projCtx _branch -> pure options})
129+
(FetchOptions (\_codebase _projCtx _branch -> pure options))
130130

131131
-- | Combine multiple option fetchers into one resolver.
132132
multiResolver :: [OptionFetcher] -> FZFResolver
133133
multiResolver resolvers =
134134
let getOptions :: Codebase IO Symbol Ann -> PP.ProjectPath -> Branch0 IO -> IO [Text]
135135
getOptions codebase projCtx searchBranch0 = do
136136
List.nubOrd <$> foldMapM (\f -> f codebase projCtx searchBranch0) resolvers
137-
in (FZFResolver {getOptions})
137+
in (FetchOptions getOptions)
138138

139139
definitionResolver :: FZFResolver
140-
definitionResolver = FZFResolver {getOptions = definitionOptions}
140+
definitionResolver = FetchOptions definitionOptions
141141

142142
typeDefinitionResolver :: FZFResolver
143-
typeDefinitionResolver = FZFResolver {getOptions = typeDefinitionOptions}
143+
typeDefinitionResolver = FetchOptions typeDefinitionOptions
144144

145145
termDefinitionResolver :: FZFResolver
146-
termDefinitionResolver = FZFResolver {getOptions = termDefinitionOptions}
146+
termDefinitionResolver = FetchOptions termDefinitionOptions
147147

148148
namespaceResolver :: FZFResolver
149-
namespaceResolver = FZFResolver {getOptions = namespaceOptions}
149+
namespaceResolver = FetchOptions namespaceOptions
150150

151151
namespaceOrDefinitionResolver :: FZFResolver
152152
namespaceOrDefinitionResolver = multiResolver [definitionOptions, namespaceOptions]
153153

154154
projectDependencyResolver :: FZFResolver
155-
projectDependencyResolver = FZFResolver {getOptions = projectDependencyOptions}
155+
projectDependencyResolver = FetchOptions projectDependencyOptions
156156

157157
-- | A project name, branch name, or both.
158158
projectAndOrBranchArg :: FZFResolver
@@ -162,13 +162,13 @@ projectOrBranchResolver :: FZFResolver
162162
projectOrBranchResolver = multiResolver [projectBranchOptions, namespaceOptions]
163163

164164
projectBranchResolver :: FZFResolver
165-
projectBranchResolver = FZFResolver {getOptions = projectBranchOptions}
165+
projectBranchResolver = FetchOptions projectBranchOptions
166166

167167
projectBranchWithinCurrentProjectResolver :: FZFResolver
168-
projectBranchWithinCurrentProjectResolver = FZFResolver {getOptions = projectBranchOptionsWithinCurrentProject}
168+
projectBranchWithinCurrentProjectResolver = FetchOptions projectBranchOptionsWithinCurrentProject
169169

170170
projectNameResolver :: FZFResolver
171-
projectNameResolver = FZFResolver {getOptions = projectNameOptions}
171+
projectNameResolver = FetchOptions projectNameOptions
172172

173173
-- | All possible local project names
174174
-- E.g. '@unison/base'

unison-cli/src/Unison/CommandLine/FuzzySelect.hs

Lines changed: 66 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ module Unison.CommandLine.FuzzySelect
77
isFZFInstalled,
88
fzfPathEnvVar,
99
Options (..),
10+
FuzzySelections (..),
1011
defaultOptions,
1112
)
1213
where
1314

14-
import Control.Monad.Except (runExceptT, throwError)
15+
import Control.Monad.Except (throwError)
16+
import Control.Monad.Trans.Except
1517
import Data.Set qualified as Set
1618
import Data.Text qualified as Text
1719
import Data.Text.IO qualified as Text
@@ -20,6 +22,7 @@ import System.Environment (lookupEnv)
2022
import System.IO (BufferMode (NoBuffering), hPutStrLn, stderr)
2123
import System.IO.Unsafe (unsafePerformIO)
2224
import Unison.Prelude
25+
import Unison.Util.Monoid qualified as Monoid
2326
import UnliftIO qualified
2427
import UnliftIO.Directory (findExecutable)
2528
import UnliftIO.Exception (bracket)
@@ -57,28 +60,33 @@ defaultOptions =
5760
}
5861

5962
-- | Convert options into command-line args for fzf
60-
optsToArgs :: Options -> [String]
61-
optsToArgs opts =
63+
optsToArgs :: Options -> Bool -> [String]
64+
optsToArgs opts useNumberings =
6265
defaultArgs <> case opts of
6366
Options {allowMultiSelect = True} -> ["-m"]
6467
_ -> []
6568
where
6669
defaultArgs =
67-
-- Don't show or match on the first column of input.
70+
-- When using numberings, don't show or match on the first column of input.
6871
-- This allows us to prepend each line with a number, and use that number to determine
6972
-- which values from the input list were selected.
70-
[ "--with-nth",
71-
"2..",
72-
-- Use only half the screen (it's nice to see what you were working on when searching)
73-
"--height=50%",
74-
-- But if 50% of the screen is too small, ensure show at least 10 results.
75-
"--min-height=10"
76-
]
73+
Monoid.whenM
74+
useNumberings
75+
["--with-nth", "2.."]
76+
<> [ -- Use only half the screen (it's nice to see what you were working on when searching)
77+
"--height=50%",
78+
-- But if 50% of the screen is too small, ensure show at least 10 results.
79+
"--min-height=10"
80+
]
81+
82+
data FuzzySelections a where
83+
SelectFromChoices :: (a -> Text) -> [a] -> FuzzySelections a
84+
SelectFiles :: FuzzySelections Text
7785

7886
-- | Allows prompting the user to interactively fuzzy-select a result from a list of options, currently shells out to `fzf` under the hood.
7987
-- If fzf is missing, or an error (other than ctrl-c) occurred, returns Nothing.
80-
fuzzySelect :: forall a. Options -> (a -> Text) -> [a] -> IO (Maybe [a])
81-
fuzzySelect opts intoSearchText choices =
88+
fuzzySelect :: forall a. Options -> FuzzySelections a -> IO (Maybe [a])
89+
fuzzySelect opts selections =
8290
UnliftIO.handleAny handleException
8391
. handleError
8492
. restoreBuffering
@@ -88,12 +96,36 @@ fuzzySelect opts intoSearchText choices =
8896
liftIO fzfExecutable >>= \case
8997
Nothing -> throwError "I couldn't find the `fzf` executable on your path, consider installing `fzf` to enable fuzzy searching."
9098
Just fzfPath -> pure fzfPath
91-
let fzfArgs :: [String] =
92-
optsToArgs opts
93-
let numberedChoices :: [(Int, a)] =
94-
zip [0 ..] choices
95-
let searchTexts :: [Text] =
96-
(\(n, ch) -> tShow (n) <> " " <> intoSearchText ch) <$> numberedChoices
99+
case selections of
100+
SelectFromChoices intoSearchText choices -> do
101+
let fzfArgs :: [String] =
102+
optsToArgs opts True
103+
let numberedChoices :: [(Int, a)] =
104+
zip [0 ..] choices
105+
let searchTexts :: [Text] =
106+
(\(n, ch) -> tShow (n) <> " " <> intoSearchText ch) <$> numberedChoices
107+
108+
result <- fzfWithChoices fzfPath fzfArgs searchTexts
109+
-- Since we prefixed every search term with its number earlier, we know each result
110+
-- is prefixed with a number, we need to parse it and use it to select the matching
111+
-- value from our input list.
112+
pure $ case result of
113+
Left _ -> Nothing
114+
Right selections ->
115+
selections
116+
& mapMaybe (readMaybe @Int . Text.unpack . Text.takeWhile (/= ' '))
117+
& Set.fromList
118+
& ( \selectedNumbers ->
119+
numberedChoices
120+
& mapMaybe (\(n, a) -> if n `Set.member` selectedNumbers then Just a else Nothing)
121+
)
122+
& Just
123+
SelectFiles -> do
124+
let fzfArgs :: [String] = optsToArgs opts False
125+
eitherToMaybe <$> fzfFileSelector fzfPath fzfArgs
126+
where
127+
fzfWithChoices :: FilePath -> [String] -> [Text] -> ExceptT Text IO (Either SomeException [Text])
128+
fzfWithChoices fzfPath fzfArgs searchTexts = do
97129
let fzfProc :: Proc.CreateProcess =
98130
(Proc.proc fzfPath fzfArgs)
99131
{ Proc.std_in = Proc.CreatePipe,
@@ -104,28 +136,27 @@ fuzzySelect opts intoSearchText choices =
104136
-- Generally no-buffering is helpful for highly interactive processes.
105137
hSetBuffering stdin NoBuffering
106138
hSetBuffering stdin' NoBuffering
107-
result <- liftIO . UnliftIO.tryAny $ do
139+
liftIO . UnliftIO.tryAny $ do
108140
-- Dump the search terms into fzf's stdin
109141
traverse_ (Text.hPutStrLn stdin') searchTexts
110142
-- Wire up the interactive terminal to fzf now that the inputs have been loaded.
111143
hDuplicateTo stdin stdin'
112144
void $ Proc.waitForProcess procHandle
113145
Text.lines <$> liftIO (Text.hGetContents stdout')
114-
-- Since we prefixed every search term with its number earlier, we know each result
115-
-- is prefixed with a number, we need to parse it and use it to select the matching
116-
-- value from our input list.
117-
pure $ case result of
118-
Left _ -> Nothing
119-
Right selections ->
120-
selections
121-
& mapMaybe (readMaybe @Int . Text.unpack . Text.takeWhile (/= ' '))
122-
& Set.fromList
123-
& ( \selectedNumbers ->
124-
numberedChoices
125-
& mapMaybe (\(n, a) -> if n `Set.member` selectedNumbers then Just a else Nothing)
126-
)
127-
& Just
128-
where
146+
fzfFileSelector :: FilePath -> [String] -> ExceptT Text IO (Either SomeException [Text])
147+
fzfFileSelector fzfPath fzfArgs = do
148+
let fzfProc :: Proc.CreateProcess =
149+
(Proc.proc fzfPath fzfArgs)
150+
{ Proc.std_in = Proc.Inherit,
151+
Proc.std_out = Proc.CreatePipe,
152+
Proc.delegate_ctlc = True
153+
}
154+
(_stdin', Just stdout', _, procHandle) <- Proc.createProcess fzfProc
155+
-- Generally no-buffering is helpful for highly interactive processes.
156+
hSetBuffering stdin NoBuffering
157+
liftIO . UnliftIO.tryAny $ do
158+
void $ Proc.waitForProcess procHandle
159+
Text.lines <$> liftIO (Text.hGetContents stdout')
129160
handleException :: SomeException -> IO (Maybe [a])
130161
handleException err = traceShowM err *> hPutStrLn stderr "Oops, something went wrong. No input selected." *> pure Nothing
131162
handleError :: IO (Either Text (Maybe [a])) -> IO (Maybe [a])

unison-cli/src/Unison/CommandLine/InputPatterns.hs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1831,7 +1831,7 @@ debugFormat =
18311831
"debug.format"
18321832
[]
18331833
I.Hidden
1834-
(Parameters [] $ Optional [("source-file", filePathArg)] Nothing)
1834+
(Parameters [] $ Optional [("source file", filePathArg)] Nothing)
18351835
( P.lines
18361836
[ P.wrap $ "This command can be used to test ucm's file formatter on the latest typechecked file.",
18371837
makeExample' debugFormat
@@ -2032,7 +2032,7 @@ syncToFile =
20322032
aliases = [],
20332033
visibility = I.Visible,
20342034
params =
2035-
Parameters [("file-path", filePathArg)] $
2035+
Parameters [("destination sync file", filePathArg)] $
20362036
Optional [("branch", projectAndBranchNamesArg suggestionsConfig)] Nothing,
20372037
help =
20382038
( P.wrapColumn2
@@ -2064,7 +2064,7 @@ syncFromFile =
20642064
aliases = [],
20652065
visibility = I.Visible,
20662066
params =
2067-
Parameters [("file-path", filePathArg), ("destination branch", projectAndBranchNamesArg suggestionsConfig)] $
2067+
Parameters [("file to sync from", filePathArg), ("destination branch", projectAndBranchNamesArg suggestionsConfig)] $
20682068
Optional [] Nothing,
20692069
help =
20702070
( P.wrapColumn2
@@ -2093,9 +2093,9 @@ syncFromCodebase =
20932093
visibility = I.Visible,
20942094
params =
20952095
Parameters
2096-
[ ("codebase-location", filePathArg),
2097-
("branch-to-sync", projectAndBranchNamesArg suggestionsConfig),
2098-
("destination-branch", projectAndBranchNamesArg suggestionsConfig)
2096+
[ ("codebase location", directoryPathArg),
2097+
("branch to sync", projectAndBranchNamesArg suggestionsConfig),
2098+
("destination branch", projectAndBranchNamesArg suggestionsConfig)
20992099
]
21002100
$ Optional [] Nothing,
21012101
help =
@@ -2883,7 +2883,7 @@ docsToHtml =
28832883
"docs.to-html"
28842884
[]
28852885
I.Visible
2886-
(Parameters [("namespace", branchRelativePathArg), ("output directory", filePathArg)] $ Optional [] Nothing)
2886+
(Parameters [("namespace", branchRelativePathArg), ("output directory", directoryPathArg)] $ Optional [] Nothing)
28872887
( P.wrapColumn2
28882888
[ ( makeExample docsToHtml [".path.to.ns", "doc-dir"],
28892889
"Render all docs contained within the namespace `.path.to.ns`, no matter how deep, to html files in `doc-dir` in the directory UCM was run from."
@@ -3763,7 +3763,16 @@ filePathArg :: ParameterType
37633763
filePathArg =
37643764
ParameterType
37653765
{ typeName = "file-path",
3766-
suggestions = noCompletions,
3766+
suggestions = \prefix _ _ _ -> filenameCompletion prefix,
3767+
fzfResolver = Just I.DefaultFZFFileSearch,
3768+
isStructured = False
3769+
}
3770+
3771+
directoryPathArg :: ParameterType
3772+
directoryPathArg =
3773+
ParameterType
3774+
{ typeName = "directory-path",
3775+
suggestions = \prefix _ _ _ -> filenameCompletion prefix,
37673776
fzfResolver = Nothing,
37683777
isStructured = False
37693778
}

0 commit comments

Comments
 (0)