Skip to content

Commit

Permalink
Merge pull request #4535 from unisonweb/cp/edit-namespace
Browse files Browse the repository at this point in the history
Add `edit.namespace` command
  • Loading branch information
ChrisPenner authored Jan 23, 2024
2 parents 7053432 + 0f84bd0 commit 58296a7
Show file tree
Hide file tree
Showing 9 changed files with 337 additions and 46 deletions.
53 changes: 8 additions & 45 deletions unison-cli/src/Unison/Codebase/Editor/HandleInput.hs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{-# HLINT ignore "Use tuple-section" #-}
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}

{-# HLINT ignore "Use tuple-section" #-}
module Unison.Codebase.Editor.HandleInput
( loop,
)
Expand Down Expand Up @@ -42,13 +42,11 @@ import U.Codebase.Sqlite.ProjectBranch qualified as Sqlite
import U.Codebase.Sqlite.Queries qualified as Queries
import Unison.ABT qualified as ABT
import Unison.Builtin qualified as Builtin
import Unison.Builtin.Decls qualified as DD
import Unison.Builtin.Terms qualified as Builtin
import Unison.Cli.Monad (Cli)
import Unison.Cli.Monad qualified as Cli
import Unison.Cli.MonadUtils qualified as Cli
import Unison.Cli.NamesUtils qualified as Cli
import Unison.Cli.Pretty qualified as Pretty
import Unison.Cli.PrettyPrintUtils qualified as Cli
import Unison.Cli.ProjectUtils qualified as ProjectUtils
import Unison.Cli.TypeCheck (typecheckTerm)
Expand All @@ -70,6 +68,7 @@ import Unison.Codebase.Editor.HandleInput.BranchRename (handleBranchRename)
import Unison.Codebase.Editor.HandleInput.Branches (handleBranches)
import Unison.Codebase.Editor.HandleInput.DeleteBranch (handleDeleteBranch)
import Unison.Codebase.Editor.HandleInput.DeleteProject (handleDeleteProject)
import Unison.Codebase.Editor.HandleInput.EditNamespace (handleEditNamespace)
import Unison.Codebase.Editor.HandleInput.FindAndReplace (handleStructuredFindI, handleStructuredFindReplaceI)
import Unison.Codebase.Editor.HandleInput.FormatFile qualified as Format
import Unison.Codebase.Editor.HandleInput.Load (EvalMode (Sandboxed), evalUnisonFile, handleLoad, loadUnisonFile)
Expand All @@ -89,6 +88,7 @@ import Unison.Codebase.Editor.HandleInput.Push (handleGist, handlePushRemoteBran
import Unison.Codebase.Editor.HandleInput.ReleaseDraft (handleReleaseDraft)
import Unison.Codebase.Editor.HandleInput.Run (handleRun)
import Unison.Codebase.Editor.HandleInput.RuntimeUtils qualified as RuntimeUtils
import Unison.Codebase.Editor.HandleInput.ShowDefinition (showDefinitions)
import Unison.Codebase.Editor.HandleInput.TermResolution (resolveCon, resolveMainRef, resolveTermRef)
import Unison.Codebase.Editor.HandleInput.Tests qualified as Tests
import Unison.Codebase.Editor.HandleInput.UI (openUI)
Expand Down Expand Up @@ -790,6 +790,7 @@ loop e = do
DisplayI outputLoc namesToDisplay -> do
traverse_ (displayI outputLoc) namesToDisplay
ShowDefinitionI outputLoc showDefinitionScope query -> handleShowDefinition outputLoc showDefinitionScope query
EditNamespaceI paths -> handleEditNamespace LatestFileLocation paths
FindPatchI -> do
branch <- Cli.getCurrentBranch0
let patches =
Expand Down Expand Up @@ -1393,6 +1394,8 @@ inputDescription input =
ReleaseDraftI {} -> wat
ShowDefinitionByPrefixI {} -> wat
ShowDefinitionI {} -> wat
EditNamespaceI paths ->
pure $ Text.unwords ("edit.namespace" : (Path.toText <$> paths))
ShowReflogI {} -> wat
SwitchBranchI {} -> wat
TestI {} -> wat
Expand Down Expand Up @@ -1649,7 +1652,7 @@ handleDiffNamespaceToPatch description input = do
-- | Handle a @ShowDefinitionI@ input command, i.e. `view` or `edit`.
handleShowDefinition :: OutputLocation -> ShowDefinitionScope -> NonEmpty (HQ.HashQualified Name) -> Cli ()
handleShowDefinition outputLoc showDefinitionScope query = do
Cli.Env {codebase, writeSource} <- ask
Cli.Env {codebase} <- ask
hqLength <- Cli.runTransaction Codebase.hashLength
let hasAbsoluteQuery = any (any Name.isAbsolute) query
(names, unbiasedPPED) <- case (hasAbsoluteQuery, showDefinitionScope) of
Expand All @@ -1675,55 +1678,15 @@ handleShowDefinition outputLoc showDefinitionScope query = do
Backend.DefinitionResults terms types misses <- do
let nameSearch = NameSearch.makeNameSearch hqLength names
Cli.runTransaction (Backend.definitionsByName codebase nameSearch includeCycles Names.IncludeSuffixes (toList query))
outputPath <- getOutputPath
case outputPath of
_ | null terms && null types -> pure ()
Nothing -> do
-- If we're writing to console we don't add test-watch syntax
let isTest _ = False
let isSourceFile = False
-- No filepath, render code to console.
let renderedCodePretty = renderCodePretty pped isSourceFile isTest terms types
Cli.respond $ DisplayDefinitions renderedCodePretty
Just fp -> do
-- We build an 'isTest' check to prepend "test>" to tests in a scratch file.
testRefs <- Cli.runTransaction (Codebase.filterTermsByReferenceIdHavingType codebase (DD.testResultType mempty) (Map.keysSet terms & Set.mapMaybe Reference.toId))
let isTest r = Set.member r testRefs
let isSourceFile = True
let renderedCodePretty = renderCodePretty pped isSourceFile isTest terms types
let renderedCodeText = Text.pack $ P.toPlain 80 renderedCodePretty

-- We set latestFile to be programmatically generated, if we
-- are viewing these definitions to a file - this will skip the
-- next update for that file (which will happen immediately)
#latestFile ?= (fp, True)
liftIO $ writeSource (Text.pack fp) renderedCodeText
let numDefinitions = Map.size terms + Map.size types
Cli.respond $ LoadedDefinitionsToSourceFile fp numDefinitions
when (not (null misses)) (Cli.respond (SearchTermsNotFound misses))
showDefinitions outputLoc pped terms types misses
where
renderCodePretty pped isSourceFile isTest terms types =
P.syntaxToColor . P.sep "\n\n" $
Pretty.prettyTypeDisplayObjects pped types <> Pretty.prettyTermDisplayObjects pped isSourceFile isTest terms
-- `view`: don't include cycles; `edit`: include cycles
includeCycles =
case outputLoc of
ConsoleLocation -> Backend.DontIncludeCycles
FileLocation _ -> Backend.IncludeCycles
LatestFileLocation -> Backend.IncludeCycles

-- Get the file path to send the definition(s) to. `Nothing` means the terminal.
getOutputPath :: Cli (Maybe FilePath)
getOutputPath =
case outputLoc of
ConsoleLocation -> pure Nothing
FileLocation path -> pure (Just path)
LatestFileLocation -> do
loopState <- State.get
pure case loopState ^. #latestFile of
Nothing -> Just "scratch.u"
Just (path, _) -> Just path

-- todo: compare to `getHQTerms` / `getHQTypes`. Is one universally better?
resolveHQToLabeledDependencies :: HQ.HashQualified Name -> Cli (Set LabeledDependency)
resolveHQToLabeledDependencies = \case
Expand Down
67 changes: 67 additions & 0 deletions unison-cli/src/Unison/Codebase/Editor/HandleInput/EditNamespace.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
module Unison.Codebase.Editor.HandleInput.EditNamespace (handleEditNamespace) where

import Control.Monad.Reader
import Data.List.Extra qualified as List
import Data.Map qualified as Map
import Unison.Cli.Monad (Cli)
import Unison.Cli.Monad qualified as Cli
import Unison.Cli.MonadUtils qualified as Cli
import Unison.Cli.PrettyPrintUtils qualified as NamesUtils
import Unison.Codebase qualified as Codebase
import Unison.Codebase.Branch qualified as Branch
import Unison.Codebase.Branch.Names qualified as Branch
import Unison.Codebase.Editor.HandleInput.ShowDefinition (showDefinitions)
import Unison.Codebase.Editor.Input (OutputLocation (..))
import Unison.Codebase.Path (Path)
import Unison.Codebase.Path qualified as Path
import Unison.Names qualified as Names
import Unison.Prelude
import Unison.Server.Backend qualified as Backend
import Unison.Util.Monoid (foldMapM)

handleEditNamespace :: OutputLocation -> [Path] -> Cli ()
handleEditNamespace outputLoc inputPaths = do
Cli.Env {codebase} <- ask
currentBranch <- Cli.getCurrentBranch0
ppe <- NamesUtils.currentPrettyPrintEnvDecl
let paths =
if null inputPaths
then [Path.empty]
else inputPaths
let allNamesToEdit =
(List.nubOrd paths) & foldMap \path ->
let b = Branch.withoutLib $ Branch.getAt0 path currentBranch
names = (Branch.toNames b)
prefixedNames = case Path.toName path of
Nothing -> names
Just pathPrefix -> Names.prefix0 pathPrefix names
in prefixedNames
let termRefs = Names.termReferences allNamesToEdit
-- We only need to (optionally) include cycles for type references, not term references,
-- because 'update' is smart enough to patch-up cycles as expected for terms.
let typeRefsWithoutCycles = Names.typeReferences allNamesToEdit
typeRefs <- Cli.runTransaction $
case includeCycles of
Backend.IncludeCycles -> foldMapM Codebase.componentReferencesForReference typeRefsWithoutCycles
Backend.DontIncludeCycles -> pure typeRefsWithoutCycles

terms <-
termRefs
& foldMapM \ref ->
Map.singleton ref <$> Backend.displayTerm codebase ref
& Cli.runTransaction

types <-
typeRefs
& foldMapM \ref ->
Map.singleton ref <$> Backend.displayType codebase ref
& Cli.runTransaction
let misses = []
showDefinitions outputLoc ppe terms types misses
where
-- `view`: don't include cycles; `edit`: include cycles
includeCycles =
case outputLoc of
ConsoleLocation -> Backend.DontIncludeCycles
FileLocation _ -> Backend.IncludeCycles
LatestFileLocation -> Backend.IncludeCycles
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
module Unison.Codebase.Editor.HandleInput.ShowDefinition (showDefinitions) where

import Control.Lens
import Control.Monad.Reader (ask)
import Control.Monad.State qualified as State
import Data.Map qualified as Map
import Data.Set qualified as Set
import Data.Text qualified as Text
import Unison.Builtin.Decls qualified as DD
import Unison.Cli.Monad (Cli)
import Unison.Cli.Monad qualified as Cli
import Unison.Cli.Pretty qualified as Pretty
import Unison.Codebase qualified as Codebase
import Unison.Codebase.Editor.DisplayObject (DisplayObject)
import Unison.Codebase.Editor.Input
import Unison.Codebase.Editor.Output
import Unison.DataDeclaration (Decl)
import Unison.HashQualified qualified as HQ
import Unison.Name (Name)
import Unison.Parser.Ann (Ann)
import Unison.Prelude
import Unison.PrettyPrintEnvDecl qualified as PPED
import Unison.Reference qualified as Reference
import Unison.Symbol (Symbol)
import Unison.Term (Term)
import Unison.Type (Type)
import Unison.Util.Pretty qualified as Pretty
import Unison.Util.Set qualified as Set

-- | Show the provided definitions to console or scratch file.
-- The caller is responsible for ensuring that the definitions include cycles if that's
-- the desired behavior.
showDefinitions ::
OutputLocation ->
PPED.PrettyPrintEnvDecl ->
(Map Reference.Reference (DisplayObject (Type Symbol Ann) (Term Symbol Ann))) ->
( Map
Reference.Reference
(DisplayObject () (Decl Symbol Ann))
) ->
[HQ.HashQualified Name] ->
Cli ()
showDefinitions outputLoc pped terms types misses = do
Cli.Env {codebase, writeSource} <- ask
outputPath <- getOutputPath
case outputPath of
_ | null terms && null types -> pure ()
Nothing -> do
-- If we're writing to console we don't add test-watch syntax
let isTest _ = False
let isSourceFile = False
-- No filepath, render code to console.
let renderedCodePretty = renderCodePretty pped isSourceFile isTest terms types
Cli.respond $ DisplayDefinitions renderedCodePretty
Just fp -> do
-- We build an 'isTest' check to prepend "test>" to tests in a scratch file.
testRefs <- Cli.runTransaction (Codebase.filterTermsByReferenceIdHavingType codebase (DD.testResultType mempty) (Map.keysSet terms & Set.mapMaybe Reference.toId))
let isTest r = Set.member r testRefs
let isSourceFile = True
let renderedCodePretty = renderCodePretty pped isSourceFile isTest terms types
let renderedCodeText = Text.pack $ Pretty.toPlain 80 renderedCodePretty

-- We set latestFile to be programmatically generated, if we
-- are viewing these definitions to a file - this will skip the
-- next update for that file (which will happen immediately)
#latestFile ?= (fp, True)
liftIO $ writeSource (Text.pack fp) renderedCodeText
let numDefinitions = Map.size terms + Map.size types
Cli.respond $ LoadedDefinitionsToSourceFile fp numDefinitions
when (not (null misses)) (Cli.respond (SearchTermsNotFound misses))
where
-- Get the file path to send the definition(s) to. `Nothing` means the terminal.
getOutputPath :: Cli (Maybe FilePath)
getOutputPath =
case outputLoc of
ConsoleLocation -> pure Nothing
FileLocation path -> pure (Just path)
LatestFileLocation -> do
loopState <- State.get
pure case loopState ^. #latestFile of
Nothing -> Just "scratch.u"
Just (path, _) -> Just path

renderCodePretty pped isSourceFile isTest terms types =
Pretty.syntaxToColor . Pretty.sep "\n\n" $
Pretty.prettyTypeDisplayObjects pped types <> Pretty.prettyTermDisplayObjects pped isSourceFile isTest terms
1 change: 1 addition & 0 deletions unison-cli/src/Unison/Codebase/Editor/Input.hs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ data Input
| CloneI ProjectAndBranchNames (Maybe ProjectAndBranchNames)
| ReleaseDraftI Semver
| UpgradeI !NameSegment !NameSegment
| EditNamespaceI [Path.Path]
deriving (Eq, Show)

-- | The source of a `branch` command: what to make the new branch from.
Expand Down
16 changes: 16 additions & 0 deletions unison-cli/src/Unison/CommandLine/InputPatterns.hs
Original file line number Diff line number Diff line change
Expand Up @@ -1879,6 +1879,21 @@ edit =
[] -> Left (I.help edit)
}

editNamespace :: InputPattern
editNamespace =
InputPattern
{ patternName = "edit.namespace",
aliases = [],
visibility = I.Visible,
args = [("namespace to load definitions from", ZeroPlus, namespaceArg)],
help =
P.lines
[ "`edit.namespace` will load all terms and types contained within the current namespace into your scratch file. This includes definitions in namespaces, but excludes libraries.",
"`edit.namespace ns1 ns2 ...` loads the terms and types contained within the provided namespaces."
],
parse = Right . Input.EditNamespaceI . fmap (Path.fromText . Text.pack)
}

topicNameArg :: ArgumentType
topicNameArg =
let topics = Map.keys helpTopicsMap
Expand Down Expand Up @@ -3014,6 +3029,7 @@ validInputs =
docs,
docsToHtml,
edit,
editNamespace,
execute,
fetchScheme,
find,
Expand Down
2 changes: 2 additions & 0 deletions unison-cli/unison-cli.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ library
Unison.Codebase.Editor.HandleInput.BranchRename
Unison.Codebase.Editor.HandleInput.DeleteBranch
Unison.Codebase.Editor.HandleInput.DeleteProject
Unison.Codebase.Editor.HandleInput.EditNamespace
Unison.Codebase.Editor.HandleInput.FindAndReplace
Unison.Codebase.Editor.HandleInput.FormatFile
Unison.Codebase.Editor.HandleInput.Load
Expand All @@ -71,6 +72,7 @@ library
Unison.Codebase.Editor.HandleInput.ReleaseDraft
Unison.Codebase.Editor.HandleInput.Run
Unison.Codebase.Editor.HandleInput.RuntimeUtils
Unison.Codebase.Editor.HandleInput.ShowDefinition
Unison.Codebase.Editor.HandleInput.TermResolution
Unison.Codebase.Editor.HandleInput.Tests
Unison.Codebase.Editor.HandleInput.UI
Expand Down
3 changes: 2 additions & 1 deletion unison-core/src/Unison/Names.hs
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,9 @@ referenceIds Names {terms, types} =
fromTerms = Set.mapMaybe Referent.toReferenceId (Relation.ran terms)
fromTypes = Set.mapMaybe Reference.toId (Relation.ran types)

-- | Returns all constructor term references. Constructors are omitted.
termReferences :: Names -> Set TermReference
termReferences Names {..} = Set.map Referent.toReference $ R.ran terms
termReferences Names {..} = Set.mapMaybe Referent.toTermReference $ R.ran terms

typeReferences :: Names -> Set TypeReference
typeReferences Names {..} = R.ran types
Expand Down
41 changes: 41 additions & 0 deletions unison-src/transcripts/edit-namespace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
```ucm:hide
.lib> builtins.mergeio
```

```unison:hide
{{ ping doc }}
nested.cycle.ping n = n Nat.+ pong n
{{ pong doc }}
nested.cycle.pong n = n Nat.+ ping n
toplevel = "hi"
simple.x = 10
simple.y = 20
-- Shouldn't edit things in lib
lib.project.ignoreMe = 30
```

```ucm:hide
.> add
```

Edit current namespace

```ucm
.simple> edit.namespace
```

Edit should hit things recursively

```ucm
.> edit.namespace
```

Edit should handle multiple explicit paths at once.

```ucm
.> edit.namespace nested.cycle simple
```
Loading

0 comments on commit 58296a7

Please sign in to comment.