From 89e9cc4cf35e98f1909cab18424f0286cb3e461c Mon Sep 17 00:00:00 2001 From: Chris Penner Date: Mon, 8 Jan 2024 14:18:19 -0800 Subject: [PATCH 1/2] Remove UCM globbing as a feature --- unison-cli/src/Unison/CommandLine.hs | 18 +-- unison-cli/src/Unison/CommandLine/Globbing.hs | 148 ------------------ 2 files changed, 2 insertions(+), 164 deletions(-) delete mode 100644 unison-cli/src/Unison/CommandLine/Globbing.hs diff --git a/unison-cli/src/Unison/CommandLine.hs b/unison-cli/src/Unison/CommandLine.hs index ecc37e9c36..3c551ef232 100644 --- a/unison-cli/src/Unison/CommandLine.hs +++ b/unison-cli/src/Unison/CommandLine.hs @@ -50,7 +50,6 @@ import Unison.Codebase.Path qualified as Path import Unison.Codebase.Watch qualified as Watch import Unison.CommandLine.FZFResolvers qualified as FZFResolvers import Unison.CommandLine.FuzzySelect qualified as Fuzzy -import Unison.CommandLine.Globbing qualified as Globbing import Unison.CommandLine.InputPattern (InputPattern (..)) import Unison.CommandLine.InputPattern qualified as InputPattern import Unison.Parser.Ann (Ann) @@ -122,7 +121,7 @@ nothingTodo = emojiNote "😶" parseInput :: Codebase IO Symbol Ann -> IO (Branch0 IO) -> - -- | Current path from root, used to expand globs + -- | Current path from root Path.Absolute -> -- | Numbered arguments [String] -> @@ -147,20 +146,7 @@ parseInput codebase getRoot currentPath numberedArgs patterns segments = runExce let expandedNumbers :: [String] expandedNumbers = foldMap (expandNumber numberedArgs) args - expandedGlobs <- ifor expandedNumbers $ \i arg -> do - if Globbing.containsGlob arg - then do - rootBranch <- liftIO getRoot - let targets = case InputPattern.argType pat i of - Just argT -> InputPattern.globTargets argT - Nothing -> mempty - case Globbing.expandGlobs targets rootBranch currentPath arg of - -- No globs encountered - Nothing -> pure [arg] - Just [] -> throwE $ "No matches for: " <> fromString arg - Just matches -> pure matches - else pure [arg] - lift (fzfResolve codebase projCtx getCurrentBranch0 pat (concat expandedGlobs)) >>= \case + lift (fzfResolve codebase projCtx getCurrentBranch0 pat (concat expandedNumbers)) >>= \case Left (NoFZFResolverForArgumentType _argDesc) -> throwError help Left (NoFZFOptions argDesc) -> throwError (noCompletionsMessage argDesc) Left FZFCancelled -> pure Nothing diff --git a/unison-cli/src/Unison/CommandLine/Globbing.hs b/unison-cli/src/Unison/CommandLine/Globbing.hs deleted file mode 100644 index f9bbdc97ab..0000000000 --- a/unison-cli/src/Unison/CommandLine/Globbing.hs +++ /dev/null @@ -1,148 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE ViewPatterns #-} - --- | Provides Globbing for selecting types, terms and namespaces using wildcards. -module Unison.CommandLine.Globbing - ( expandGlobs, - containsGlob, - TargetType (..), - ) -where - -import Control.Lens as Lens hiding (noneOf) -import Data.Either qualified as Either -import Data.Set qualified as Set -import Data.Text qualified as Text -import Unison.Codebase.Branch (Branch0) -import Unison.Codebase.Branch qualified as Branch -import Unison.Codebase.Path qualified as Path -import Unison.NameSegment (NameSegment (NameSegment)) -import Unison.NameSegment qualified as NameSegment -import Unison.Prelude -import Unison.Util.Monoid qualified as Monoid -import Unison.Util.Relation qualified as Relation -import Unison.Util.Star3 qualified as Star3 - --- | Possible targets which a glob may select. -data TargetType - = Type - | Term - | Namespace - deriving (Eq, Ord, Show) - --- | Glob paths are always relative to some branch. -type GlobPath = [Either NameSegment GlobArg] - --- | Represents a name segment containing a glob pattern --- e.g. start?end -> GlobArg "start" "end" -data GlobArg = GlobArg - { namespacePrefix :: Text, - namespaceSuffix :: Text - } - deriving (Show) - --- | Constructs a namespace "matcher" from a 'GlobArg' -globPredicate :: Either NameSegment GlobArg -> (NameSegment -> Bool) -globPredicate globArg (NameSegment.toText -> ns') = - case globArg of - Left (NameSegment.toText -> ns) -> ns == ns' - Right (GlobArg prefix suffix) -> prefix `Text.isPrefixOf` ns' && suffix `Text.isSuffixOf` ns' - --- | Expands a glob into a list of paths which lead to valid targets. -expandGlobToPaths :: Set TargetType -> GlobPath -> Branch0 m -> [Path.Relative] -expandGlobToPaths targets globPath branch = - (Path.Relative . Path.fromList) <$> expandGlobToNameSegments targets branch globPath - --- | Helper for 'expandGlobToPaths' -expandGlobToNameSegments :: forall m. Set TargetType -> Branch0 m -> GlobPath -> [[NameSegment]] -expandGlobToNameSegments targets branch globPath = - case globPath of - -- The glob path was empty; it yields no matches. - [] -> [] - -- If we're at the end of the path, add any targets which match. - [segment] -> - Monoid.whenM (Set.member Term targets) matchingTerms - <> Monoid.whenM (Set.member Type targets) matchingTypes - <> Monoid.whenM (Set.member Namespace targets) matchingNamespaces - where - predicate :: NameSegment -> Bool - predicate = globPredicate segment - matchingNamespaces, matchingTerms, matchingTypes :: [[NameSegment]] - matchingNamespaces = branch ^.. matchingChildBranches predicate . asIndex . to (pure @[]) - matchingTerms = matchingNamesInStar predicate (Branch._terms branch) - matchingTypes = matchingNamesInStar predicate (Branch._types branch) - matchingNamesInStar :: (NameSegment -> Bool) -> Branch.Star a NameSegment -> [[NameSegment]] - matchingNamesInStar predicate star = - star - & Star3.d1 - & Relation.ran - & Set.toList - & filter predicate - & fmap (pure @[]) -- Embed each name segment into a path. - -- If we have multiple remaining segments, descend into any children matching the current - -- segment, then keep matching on the remainder of the path. - (segment : rest) -> recursiveMatches - where - nextBranches :: [(NameSegment, (Branch0 m))] - nextBranches = branch ^@.. matchingChildBranches (globPredicate segment) - recursiveMatches :: [[NameSegment]] - recursiveMatches = - foldMap (\(ns, b) -> (ns :) <$> expandGlobToNameSegments targets b rest) nextBranches - --- | Find all child branches whose name matches a predicate. -matchingChildBranches :: (NameSegment -> Bool) -> IndexedTraversal' NameSegment (Branch0 m) (Branch0 m) -matchingChildBranches keyPredicate = Branch.children0 . indices keyPredicate - --- | Expand a single glob pattern into all matching targets of the specified types. -expandGlobs :: - forall m. - Set TargetType -> - -- | Root branch - Branch0 m -> - -- | UCM's current path - Path.Absolute -> - -- | The glob string, e.g. .base.List.?.doc - String -> - -- | Nothing if arg was not a glob. - -- otherwise, fully expanded, absolute paths. E.g. [".base.List.map"] - Maybe [String] -expandGlobs targets rootBranch currentPath s = do - guard (not . null $ targets) - let (isAbsolute, globPath) = globbedPathParser (Text.pack s) - guard (any Either.isRight $ globPath) - let currentBranch :: Branch0 m - currentBranch - | isAbsolute = rootBranch - | otherwise = Branch.getAt0 (Path.unabsolute currentPath) rootBranch - let paths = expandGlobToPaths targets globPath currentBranch - let relocatedPaths - | isAbsolute = (Path.Absolute . Path.unrelative) <$> paths - | otherwise = Path.resolve currentPath <$> paths - pure (Path.convert <$> relocatedPaths) - -containsGlob :: String -> Bool -containsGlob s = - let (_, globPath) = globbedPathParser (Text.pack s) - in any Either.isRight $ globPath - --- | Parses a single name segment into a GlobArg or a bare segment according to whether --- there's a glob. --- E.g. --- "toList" -> Left (NameSegment "toList") --- "to?" -> Left (GlobArg "to" "") --- We unintuitively use '?' for glob patterns right now since they're not valid in names. -globbedPathParser :: Text -> (Bool, GlobPath) -globbedPathParser txt = - let (isAbsolute, segments) = - case Text.split (== '.') txt of - -- An initial '.' creates an empty split - ("" : segments) -> (True, segments) - (segments) -> (False, segments) - in (isAbsolute, fmap globArgParser segments) - -globArgParser :: Text -> Either NameSegment GlobArg -globArgParser txt = - case Text.split (== '?') txt of - [prefix, suffix] -> Right (GlobArg prefix suffix) - _ -> Left (NameSegment txt) From 90d89accc8b5639f5f0791a8cbcba3597b82f923 Mon Sep 17 00:00:00 2001 From: Chris Penner Date: Mon, 8 Jan 2024 14:25:57 -0800 Subject: [PATCH 2/2] Remove globbing --- unison-cli/src/Unison/CommandLine.hs | 3 +- .../src/Unison/CommandLine/InputPattern.hs | 4 - .../src/Unison/CommandLine/InputPatterns.hs | 23 ---- unison-cli/unison-cli.cabal | 3 +- unison-src/transcripts/globbing.md | 67 ---------- unison-src/transcripts/globbing.output.md | 124 ------------------ 6 files changed, 2 insertions(+), 222 deletions(-) delete mode 100644 unison-src/transcripts/globbing.md delete mode 100644 unison-src/transcripts/globbing.output.md diff --git a/unison-cli/src/Unison/CommandLine.hs b/unison-cli/src/Unison/CommandLine.hs index 3c551ef232..455ac1ff39 100644 --- a/unison-cli/src/Unison/CommandLine.hs +++ b/unison-cli/src/Unison/CommandLine.hs @@ -26,7 +26,6 @@ module Unison.CommandLine where import Control.Concurrent (forkIO, killThread) -import Control.Lens (ifor) import Control.Monad.Except import Control.Monad.Trans.Except import Data.Configurator (autoConfig, autoReload) @@ -146,7 +145,7 @@ parseInput codebase getRoot currentPath numberedArgs patterns segments = runExce let expandedNumbers :: [String] expandedNumbers = foldMap (expandNumber numberedArgs) args - lift (fzfResolve codebase projCtx getCurrentBranch0 pat (concat expandedNumbers)) >>= \case + lift (fzfResolve codebase projCtx getCurrentBranch0 pat expandedNumbers) >>= \case Left (NoFZFResolverForArgumentType _argDesc) -> throwError help Left (NoFZFOptions argDesc) -> throwError (noCompletionsMessage argDesc) Left FZFCancelled -> pure Nothing diff --git a/unison-cli/src/Unison/CommandLine/InputPattern.hs b/unison-cli/src/Unison/CommandLine/InputPattern.hs index b6006f39b9..f72506bab5 100644 --- a/unison-cli/src/Unison/CommandLine/InputPattern.hs +++ b/unison-cli/src/Unison/CommandLine/InputPattern.hs @@ -27,7 +27,6 @@ import Unison.Codebase (Codebase) import Unison.Codebase.Editor.Input (Input (..)) import Unison.Codebase.Path as Path import Unison.CommandLine.FZFResolvers (FZFResolver (..)) -import Unison.CommandLine.Globbing qualified as Globbing import Unison.Prelude import Unison.Util.ColorText qualified as CT import Unison.Util.Monoid (foldMapM) @@ -70,9 +69,6 @@ data ArgumentType = ArgumentType AuthenticatedHttpClient -> Path.Absolute -> -- Current path m [Line.Completion], - -- | Select which targets glob patterns may expand into for this argument. - -- An empty set disables globbing. - globTargets :: Set Globbing.TargetType, -- | If an argument is marked as required, but not provided, the fuzzy finder will be triggered if -- available. fzfResolver :: Maybe FZFResolver diff --git a/unison-cli/src/Unison/CommandLine/InputPatterns.hs b/unison-cli/src/Unison/CommandLine/InputPatterns.hs index 42ea511edf..3249475d0c 100644 --- a/unison-cli/src/Unison/CommandLine/InputPatterns.hs +++ b/unison-cli/src/Unison/CommandLine/InputPatterns.hs @@ -47,7 +47,6 @@ import Unison.Codebase.Verbosity qualified as Verbosity import Unison.CommandLine import Unison.CommandLine.Completion import Unison.CommandLine.FZFResolvers qualified as Resolvers -import Unison.CommandLine.Globbing qualified as Globbing import Unison.CommandLine.InputPattern (ArgumentType (..), InputPattern (InputPattern), IsOptional (..), unionSuggestions) import Unison.CommandLine.InputPattern qualified as I import Unison.HashQualified qualified as HQ @@ -1857,7 +1856,6 @@ topicNameArg = in ArgumentType { typeName = "topic", suggestions = \q _ _ _ -> pure (exactComplete q $ topics), - globTargets = mempty, fzfResolver = Just $ Resolvers.fuzzySelectFromList (Text.pack <$> topics) } @@ -1866,7 +1864,6 @@ codebaseServerNameArg = ArgumentType { typeName = "codebase-server", suggestions = \_ _ _ _ -> pure [], - globTargets = mempty, fzfResolver = Nothing } @@ -2814,7 +2811,6 @@ branchInputPattern = ArgumentType { typeName = "new-branch", suggestions = \_ _ _ _ -> pure [], - globTargets = mempty, fzfResolver = Nothing } suggestionsConfig = @@ -3082,7 +3078,6 @@ commandNameArg = in ArgumentType { typeName = "command", suggestions = \q _ _ _ -> pure (exactComplete q options), - globTargets = mempty, fzfResolver = Just $ Resolvers.fuzzySelectFromList (Text.pack <$> options) } @@ -3091,7 +3086,6 @@ exactDefinitionArg = ArgumentType { typeName = "definition", suggestions = \q cb _http p -> Codebase.runTransaction cb (prefixCompleteTermOrType q p), - globTargets = Set.fromList [Globbing.Term, Globbing.Type], fzfResolver = Just Resolvers.definitionResolver } @@ -3100,7 +3094,6 @@ fuzzyDefinitionQueryArg = ArgumentType { typeName = "fuzzy definition query", suggestions = \q cb _http p -> Codebase.runTransaction cb (prefixCompleteTermOrType q p), - globTargets = Set.fromList [Globbing.Term, Globbing.Type], fzfResolver = Just Resolvers.definitionResolver } @@ -3112,7 +3105,6 @@ exactDefinitionTypeQueryArg = ArgumentType { typeName = "type definition query", suggestions = \q cb _http p -> Codebase.runTransaction cb (prefixCompleteType q p), - globTargets = Set.fromList [Globbing.Type], fzfResolver = Just Resolvers.typeDefinitionResolver } @@ -3121,7 +3113,6 @@ exactDefinitionTypeOrTermQueryArg = ArgumentType { typeName = "type or term definition query", suggestions = \q cb _http p -> Codebase.runTransaction cb (prefixCompleteTermOrType q p), - globTargets = Set.fromList [Globbing.Term], fzfResolver = Just Resolvers.definitionResolver } @@ -3130,7 +3121,6 @@ exactDefinitionTermQueryArg = ArgumentType { typeName = "term definition query", suggestions = \q cb _http p -> Codebase.runTransaction cb (prefixCompleteTerm q p), - globTargets = Set.fromList [Globbing.Term], fzfResolver = Just Resolvers.termDefinitionResolver } @@ -3139,7 +3129,6 @@ patchArg = ArgumentType { typeName = "patch", suggestions = \q cb _http p -> Codebase.runTransaction cb (prefixCompletePatch q p), - globTargets = Set.fromList [], fzfResolver = Nothing } @@ -3148,7 +3137,6 @@ namespaceArg = ArgumentType { typeName = "namespace", suggestions = \q cb _http p -> Codebase.runTransaction cb (prefixCompleteNamespace q p), - globTargets = Set.fromList [Globbing.Namespace], fzfResolver = Just Resolvers.namespaceResolver } @@ -3164,7 +3152,6 @@ namespaceOrProjectBranchArg config = [ projectAndOrBranchSuggestions config, namespaceSuggestions ], - globTargets = mempty, fzfResolver = Just Resolvers.projectOrBranchResolver } @@ -3176,7 +3163,6 @@ namespaceOrDefinitionArg = namespaces <- prefixCompleteNamespace q p termsTypes <- prefixCompleteTermOrType q p pure (List.nubOrd $ namespaces <> termsTypes), - globTargets = Set.fromList [Globbing.Namespace, Globbing.Term, Globbing.Type], fzfResolver = Just Resolvers.namespaceOrDefinitionResolver } @@ -3190,7 +3176,6 @@ newNameArg = ArgumentType { typeName = "new-name", suggestions = \q cb _http p -> Codebase.runTransaction cb (prefixCompleteNamespace q p), - globTargets = mempty, fzfResolver = Nothing } @@ -3199,7 +3184,6 @@ noCompletionsArg = ArgumentType { typeName = "word", suggestions = noCompletions, - globTargets = mempty, fzfResolver = Nothing } @@ -3208,7 +3192,6 @@ filePathArg = ArgumentType { typeName = "file-path", suggestions = noCompletions, - globTargets = mempty, fzfResolver = Nothing } @@ -3227,7 +3210,6 @@ gitUrlArg = "gls" -> complete "git(git@gitlab.com:" "bbs" -> complete "git(git@bitbucket.com:" _ -> pure [], - globTargets = mempty, fzfResolver = Nothing } @@ -3247,7 +3229,6 @@ remoteNamespaceArg = "bbs" -> complete "git(git@bitbucket.com:" _ -> do sharePathCompletion http input, - globTargets = mempty, fzfResolver = Nothing } @@ -3487,7 +3468,6 @@ projectAndBranchNamesArg config = ArgumentType { typeName = "project-and-branch-names", suggestions = projectAndOrBranchSuggestions config, - globTargets = Set.empty, fzfResolver = Just Resolvers.projectAndOrBranchArg } @@ -3497,7 +3477,6 @@ projectBranchNameArg config = ArgumentType { typeName = "project-branch-name", suggestions = projectAndOrBranchSuggestions config, - globTargets = Set.empty, fzfResolver = Just Resolvers.projectBranchResolver } @@ -3507,7 +3486,6 @@ projectBranchNameWithOptionalProjectNameArg = ArgumentType { typeName = "project-branch-name-with-optional-project-name", suggestions = \_ _ _ _ -> pure [], - globTargets = Set.empty, fzfResolver = Just Resolvers.projectBranchResolver } @@ -3521,7 +3499,6 @@ projectNameArg = Codebase.runTransaction codebase do Queries.loadAllProjectsBeginningWith (Just input) pure $ map projectToCompletion projects, - globTargets = Set.empty, fzfResolver = Just $ Resolvers.multiResolver [Resolvers.projectNameOptions] } where diff --git a/unison-cli/unison-cli.cabal b/unison-cli/unison-cli.cabal index 20ae631330..44f516cdf9 100644 --- a/unison-cli/unison-cli.cabal +++ b/unison-cli/unison-cli.cabal @@ -1,6 +1,6 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.36.0. +-- This file has been generated from package.yaml by hpack version 0.35.2. -- -- see: https://github.com/sol/hpack @@ -95,7 +95,6 @@ library Unison.CommandLine.DisplayValues Unison.CommandLine.FuzzySelect Unison.CommandLine.FZFResolvers - Unison.CommandLine.Globbing Unison.CommandLine.InputPattern Unison.CommandLine.InputPatterns Unison.CommandLine.Main diff --git a/unison-src/transcripts/globbing.md b/unison-src/transcripts/globbing.md deleted file mode 100644 index 599d72ea4a..0000000000 --- a/unison-src/transcripts/globbing.md +++ /dev/null @@ -1,67 +0,0 @@ -# Globbing - -## Overview - -This allows quickly selecting terms, types, and namespaces for any "bulk" commands. - -* Currently supports up to one wildcard PER SEGMENT; Each segment can have its own wildcard if you really want and it'll still be performant. E.g. `.base.?.to?` -* Can have a prefix, suffix or infix wildcard! E.g. `to?` or `?List` or `to?With!` -* I went with `?` instead of `*` for the wildcard symbol since `?` isn't currently a valid symbol name. This may cause some confusion since it differs from bash globbing though; so if anyone has thoughts/concerns about how to better handle this I'd love to hear them. -* Commands can select which targets they want globs to expand to; e.g. `cd` should only glob for namespace, `view` should only glob to terms & types. - -## Demo - -Add some definitions which we can match over: -```unison:hide -convertToThing = 1 -convertFromThing = 2 -otherTerm = 3 - --- Nested definitions -nested.toList = 4 -nested.toMap = 5 -othernest.toList = 6 -othernest.toMap = 7 -``` - -```ucm:hide -.> add -``` - -Globbing as a prefix, infix, or suffix wildcard. - -```ucm -.> view convert? -.> view convert?Thing -.> view ?Thing -``` - -Globbing can occur in any name segment. - -```ucm -.> view ?.toList -.> view nested.to? -``` - -You may have up to one glob per name segment. - -```ucm -.> view ?.to? -``` - - -Globbing only expands to the appropriate argument type. - -E.g. `view` should not see glob expansions for namespaces. -This should expand to only the otherTerm. - -```ucm -.> view other? -``` - -Globbing should work from within a namespace with both absolute and relative patterns. - -```ucm -.nested> view .othernest.to? -.nested> view to? -``` diff --git a/unison-src/transcripts/globbing.output.md b/unison-src/transcripts/globbing.output.md deleted file mode 100644 index 2021a94ab4..0000000000 --- a/unison-src/transcripts/globbing.output.md +++ /dev/null @@ -1,124 +0,0 @@ -# Globbing - -## Overview - -This allows quickly selecting terms, types, and namespaces for any "bulk" commands. - -* Currently supports up to one wildcard PER SEGMENT; Each segment can have its own wildcard if you really want and it'll still be performant. E.g. `.base.?.to?` -* Can have a prefix, suffix or infix wildcard! E.g. `to?` or `?List` or `to?With!` -* I went with `?` instead of `*` for the wildcard symbol since `?` isn't currently a valid symbol name. This may cause some confusion since it differs from bash globbing though; so if anyone has thoughts/concerns about how to better handle this I'd love to hear them. -* Commands can select which targets they want globs to expand to; e.g. `cd` should only glob for namespace, `view` should only glob to terms & types. - -## Demo - -Add some definitions which we can match over: -```unison -convertToThing = 1 -convertFromThing = 2 -otherTerm = 3 - --- Nested definitions -nested.toList = 4 -nested.toMap = 5 -othernest.toList = 6 -othernest.toMap = 7 -``` - -Globbing as a prefix, infix, or suffix wildcard. - -```ucm -.> view convert? - - convertFromThing : ##Nat - convertFromThing = 2 - - convertToThing : ##Nat - convertToThing = 1 - -.> view convert?Thing - - convertFromThing : ##Nat - convertFromThing = 2 - - convertToThing : ##Nat - convertToThing = 1 - -.> view ?Thing - - convertFromThing : ##Nat - convertFromThing = 2 - - convertToThing : ##Nat - convertToThing = 1 - -``` -Globbing can occur in any name segment. - -```ucm -.> view ?.toList - - nested.toList : ##Nat - nested.toList = 4 - - othernest.toList : ##Nat - othernest.toList = 6 - -.> view nested.to? - - nested.toList : ##Nat - nested.toList = 4 - - nested.toMap : ##Nat - nested.toMap = 5 - -``` -You may have up to one glob per name segment. - -```ucm -.> view ?.to? - - nested.toList : ##Nat - nested.toList = 4 - - nested.toMap : ##Nat - nested.toMap = 5 - - othernest.toList : ##Nat - othernest.toList = 6 - - othernest.toMap : ##Nat - othernest.toMap = 7 - -``` -Globbing only expands to the appropriate argument type. - -E.g. `view` should not see glob expansions for namespaces. -This should expand to only the otherTerm. - -```ucm -.> view other? - - otherTerm : ##Nat - otherTerm = 3 - -``` -Globbing should work from within a namespace with both absolute and relative patterns. - -```ucm -.nested> view .othernest.to? - - .othernest.toList : ##Nat - .othernest.toList = 6 - - .othernest.toMap : ##Nat - .othernest.toMap = 7 - -.nested> view to? - - toList : ##Nat - toList = 4 - - toMap : ##Nat - toMap = 5 - -```