Skip to content

Commit 23b831f

Browse files
authored
Merge pull request #6080 from bbarker/diff_update
2 parents 10ab5de + f69290e commit 23b831f

File tree

14 files changed

+1651
-21
lines changed

14 files changed

+1651
-21
lines changed

parser-typechecker/src/Unison/PrettyPrintEnv/MonadPretty.hs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module Unison.PrettyPrintEnv.MonadPretty
22
( MonadPretty,
33
Env (..),
44
runPretty,
5+
runPrettyForDiff,
56
addTypeVars,
67
willCaptureType,
78
withBoundTerm,
@@ -25,7 +26,11 @@ data Env v = Env
2526
{ boundTerms :: !(Set v),
2627
boundTypes :: !(Set v),
2728
freeTerms :: !(Set v),
28-
ppe :: !PrettyPrintEnv
29+
ppe :: !PrettyPrintEnv,
30+
-- | When True, always use raw strings (triple-quoted) for multiline text,
31+
-- even when nested inside other expressions. This is useful for diff output
32+
-- where we want actual newlines for better line-by-line diffing.
33+
forceRawStrings :: !Bool
2934
}
3035
deriving stock (Generic)
3136

@@ -57,7 +62,22 @@ runPretty ppe m =
5762
{ boundTerms = Set.empty,
5863
boundTypes = Set.empty,
5964
freeTerms = Set.empty,
60-
ppe
65+
ppe,
66+
forceRawStrings = False
67+
}
68+
69+
-- | Like 'runPretty', but enables raw string rendering for multiline text.
70+
-- This is useful for diff output where we want actual newlines for better diffing.
71+
runPrettyForDiff :: (Var v) => PrettyPrintEnv -> Reader (Env v) a -> a
72+
runPrettyForDiff ppe m =
73+
runReader
74+
m
75+
Env
76+
{ boundTerms = Set.empty,
77+
boundTypes = Set.empty,
78+
freeTerms = Set.empty,
79+
ppe,
80+
forceRawStrings = True
6181
}
6282

6383
-- Note [Bound and free term variables]

parser-typechecker/src/Unison/Syntax/TermPrinter.hs

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module Unison.Syntax.TermPrinter
66
pretty',
77
prettyBinding,
88
prettyBinding',
9+
prettyBindingForDiff,
910
prettyBindingWithoutTypeSignature,
1011
prettyDoc2,
1112
pretty0,
@@ -262,22 +263,24 @@ pretty0
262263
-- metaprograms), then it needs to be able to print them (and then the
263264
-- parser ought to be able to parse them, to maintain symmetry.)
264265
Boolean' b -> pure . fmt S.BooleanLiteral $ if b then l "true" else l "false"
265-
Text' s
266-
| Just quotes <- useRaw s ->
267-
pure . fmt S.TextLiteral $ PP.text quotes <> "\n" <> PP.text s <> "\n" <> PP.text quotes
268-
where
269-
-- we only use this syntax if we're not wrapped in something else,
270-
-- to avoid possible round trip issues if the text ends at an odd column
271-
useRaw _ | p >= Annotation = Nothing
272-
useRaw s | Text.elem '\n' s && Text.all ok s = Just quotes
273-
useRaw _ = Nothing
274-
ok ch = isPrint ch || ch == '\n'
275-
-- Picks smallest number of surrounding """ to be unique
276-
quotes = Text.pack (replicate numQuotes '"')
277-
numQuotes = max 3 $ longestRun '"' s + 1
278-
longestRun :: Char -> Text -> Int
279-
longestRun c = maximum . (0 :) . map Text.length . filter ((== c) . Text.head) . Text.group
280-
Text' s -> pure . fmt S.TextLiteral $ l $ U.ushow s
266+
Text' s -> do
267+
env <- ask
268+
-- Use raw strings (triple-quoted) for multiline text when:
269+
-- 1. forceRawStrings is True (for diff output), OR
270+
-- 2. We're not wrapped in something else (p < Annotation)
271+
-- AND the text contains newlines and only printable chars
272+
let useRawAllowed = env.forceRawStrings || p < Annotation
273+
canUseRaw = useRawAllowed && Text.elem '\n' s && Text.all ok s
274+
ok ch = isPrint ch || ch == '\n'
275+
-- Picks smallest number of surrounding """ to be unique
276+
quotes = Text.pack (replicate numQuotes '"')
277+
numQuotes = max 3 $ longestRun '"' s + 1
278+
longestRun :: Char -> Text -> Int
279+
longestRun c = maximum . (0 :) . map Text.length . filter ((== c) . Text.head) . Text.group
280+
pure $
281+
if canUseRaw
282+
then fmt S.TextLiteral $ PP.text quotes <> "\n" <> PP.text s <> "\n" <> PP.text quotes
283+
else fmt S.TextLiteral $ l $ U.ushow s
281284
Char' c -> pure
282285
. fmt S.CharLiteral
283286
. l
@@ -974,6 +977,17 @@ prettyBinding_ ::
974977
prettyBinding_ go ppe n tm =
975978
runPretty (avoidShadowing tm ppe) . fmap go $ prettyBinding0 (ac Basement Block Map.empty MaybeDoc) n tm
976979

980+
-- | Like 'prettyBinding', but uses raw strings for multiline text literals.
981+
-- This is useful for diff output where we want actual newlines for better diffing.
982+
prettyBindingForDiff ::
983+
(Var v) =>
984+
PrettyPrintEnv ->
985+
HQ.HashQualified Name ->
986+
Term2 v at ap v a ->
987+
Pretty SyntaxText
988+
prettyBindingForDiff ppe n tm =
989+
runPrettyForDiff (avoidShadowing tm ppe) . fmap renderPrettyBinding $ prettyBinding0 (ac Basement Block Map.empty MaybeDoc) n tm
990+
977991
prettyBinding' ::
978992
(Var v) =>
979993
PrettyPrintEnv ->

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import Unison.Codebase.Editor.HandleInput.DeleteProject (handleDeleteProject)
6363
import Unison.Codebase.Editor.HandleInput.Dependencies (handleDependencies)
6464
import Unison.Codebase.Editor.HandleInput.Dependents (handleDependents)
6565
import Unison.Codebase.Editor.HandleInput.DiffBranch (handleDiffBranch)
66+
import Unison.Codebase.Editor.HandleInput.DiffUpdate qualified as DiffUpdate
6667
import Unison.Codebase.Editor.HandleInput.EditDependents (handleEditDependents)
6768
import Unison.Codebase.Editor.HandleInput.EditNamespace (handleEditNamespace)
6869
import Unison.Codebase.Editor.HandleInput.FindAndReplace (handleStructuredFindI, handleStructuredFindReplaceI, handleTextFindI)
@@ -719,6 +720,7 @@ loop e = do
719720
path0 <- Cli.getCurrentPath
720721
whenJust (Path.ascend path0) Cli.cd
721722
Update2I -> handleUpdate2
723+
DiffUpdateI -> DiffUpdate.handleDiffUpdate
722724
UpdateBuiltinsI -> Cli.respond NotImplemented
723725
UpgradeCommitI -> Cli.returnEarly (Output.Literal "The `upgrade.commit` command has been removed in favor of `update`.")
724726
UpgradeI libs -> handleUpgrade libs
@@ -889,6 +891,7 @@ inputDescription input =
889891
UiI {} -> wat
890892
UpI {} -> wat
891893
Update2I -> wat
894+
DiffUpdateI -> wat
892895
UpdateBuiltinsI -> wat
893896
UpgradeCommitI {} -> wat
894897
UpgradeI {} -> wat
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
-- | @diff.update@ input handler - shows a preview of what `update` would change.
2+
module Unison.Codebase.Editor.HandleInput.DiffUpdate
3+
( handleDiffUpdate,
4+
)
5+
where
6+
7+
import Control.Monad.Reader.Class (ask)
8+
import Data.Map.Strict qualified as Map
9+
import Data.Set qualified as Set
10+
import U.Codebase.Reference (TermReferenceId, TypeReferenceId)
11+
import Unison.Cli.Monad (Cli, Env (..))
12+
import Unison.Cli.Monad qualified as Cli
13+
import Unison.Cli.MonadUtils qualified as Cli
14+
import Unison.Cli.UpdateUtils (getNamespaceDependentsOf, hydrateRefs)
15+
import Unison.Codebase qualified as Codebase
16+
import Unison.Codebase.Branch qualified as Branch
17+
import Unison.Codebase.Branch.Names qualified as Branch
18+
import Unison.Codebase.Editor.Output qualified as Output
19+
import Unison.DataDeclaration (Decl, DeclOrBuiltin)
20+
import Unison.DeclCoherencyCheck qualified as DeclCoherencyCheck
21+
import Unison.Name (Name)
22+
import Unison.Names (Names (Names))
23+
import Unison.Names qualified as Names
24+
import Unison.OrBuiltin (OrBuiltin (..))
25+
import Unison.Parser.Ann (Ann)
26+
import Unison.Prelude
27+
import Unison.PrettyPrintEnv.Names qualified as PPE
28+
import Unison.PrettyPrintEnvDecl qualified as PPED
29+
import Unison.Reference qualified as Reference
30+
import Unison.Referent qualified as Referent
31+
import Unison.Symbol (Symbol)
32+
import Unison.Syntax.Name qualified as Name
33+
import Unison.Term (Term)
34+
import Unison.Type (Type)
35+
import Unison.UnconflictedLocalDefnsView (UnconflictedLocalDefnsView (..))
36+
import Unison.UnisonFile qualified as UF
37+
import Unison.UnisonFile.Names qualified as UF
38+
import Unison.Util.BiMultimap qualified as BiMultimap
39+
import Unison.Util.Defns (Defns (..), DefnsF)
40+
import Unison.Util.Relation qualified as Relation
41+
42+
handleDiffUpdate :: Cli ()
43+
handleDiffUpdate = do
44+
env <- ask
45+
tuf <- Cli.expectLatestTypecheckedFile
46+
currentBranch <- Cli.getCurrentBranch
47+
let currentBranch0 = Branch.head currentBranch
48+
let namesIncludingLibdeps = Branch.toNames currentBranch0
49+
50+
-- Assert that the namespace doesn't have any conflicted names
51+
unconflictedView <-
52+
Branch.asUnconflicted currentBranch0
53+
& onLeft (Cli.returnEarly . Output.ConflictedDefn)
54+
55+
-- Assert that the namespace doesn't have any incoherent decls
56+
_declNameLookup <-
57+
Cli.runTransactionWithRollback \rollback -> do
58+
Codebase.getBranchDeclNameLookup env.codebase (Branch.namespaceHash currentBranch) unconflictedView
59+
& onLeftM (rollback . Output.IncoherentDeclDuringUpdate . DeclCoherencyCheck.asOneRandomIncoherentDeclReason)
60+
61+
-- Get namespace bindings from the file (terms and types being added/updated)
62+
let namespaceBindings :: DefnsF Set Name Name
63+
namespaceBindings =
64+
bimap (Set.map Name.unsafeParseVar) (Set.map Name.unsafeParseVar) (UF.namespaceBindings tuf)
65+
66+
-- Compute new vs updated definitions
67+
let existingTermNames = BiMultimap.ran unconflictedView.defns.terms
68+
let existingTypeNames = BiMultimap.ran unconflictedView.defns.types
69+
70+
let newTermNames = Set.difference namespaceBindings.terms existingTermNames
71+
let newTypeNames = Set.difference namespaceBindings.types existingTypeNames
72+
let updatedTermNames = Set.intersection namespaceBindings.terms existingTermNames
73+
let updatedTypeNames = Set.intersection namespaceBindings.types existingTypeNames
74+
75+
-- Get dependents that would need retypechecking
76+
dependents <-
77+
Cli.runTransaction do
78+
dependents0 <-
79+
getNamespaceDependentsOf
80+
unconflictedView.defns
81+
( Names.references
82+
Names
83+
{ terms = Relation.restrictDom namespaceBindings.terms unconflictedView.names.terms,
84+
types = Relation.restrictDom namespaceBindings.types unconflictedView.names.types
85+
}
86+
)
87+
88+
-- Remove dependents that are also being updated directly by the file,
89+
-- since they'll already appear in the "updated definitions" section
90+
let dependents1 :: DefnsF (Map Name) TermReferenceId TypeReferenceId
91+
dependents1 =
92+
bimap
93+
(`Map.withoutKeys` namespaceBindings.terms)
94+
(`Map.withoutKeys` namespaceBindings.types)
95+
dependents0
96+
97+
pure dependents1
98+
99+
-- Get the terms (body + type + refId) for new and updated terms from the typechecked file
100+
-- hashTermsId returns: (ann, TermReferenceId, Maybe WatchKind, Term v a, Type v a)
101+
let fileTermsWithRefIds :: Map Name (TermReferenceId, Term Symbol Ann, Type Symbol Ann)
102+
fileTermsWithRefIds =
103+
Map.fromList
104+
[ (Name.unsafeParseVar var, (refId, term, typ))
105+
| (var, (_, refId, _, term, typ)) <- Map.toList (UF.hashTermsId tuf)
106+
]
107+
108+
let fileTerms :: Map Name (Term Symbol Ann, Type Symbol Ann)
109+
fileTerms = Map.map (\(_, term, typ) -> (term, typ)) fileTermsWithRefIds
110+
111+
let newTerms :: Map Name (Term Symbol Ann, Type Symbol Ann)
112+
newTerms = Map.restrictKeys fileTerms newTermNames
113+
114+
-- Terms from the file that are updates to existing codebase definitions (with new ref IDs)
115+
let updatedFileTerms :: Map Name (TermReferenceId, Term Symbol Ann, Type Symbol Ann)
116+
updatedFileTerms = Map.restrictKeys fileTermsWithRefIds updatedTermNames
117+
118+
-- Get the old terms from the codebase for updated definitions
119+
-- First, get the term reference IDs for the updated names
120+
let updatedTermRefIds :: Map Name TermReferenceId
121+
updatedTermRefIds =
122+
Map.fromList
123+
[ (name, refId)
124+
| name <- Set.toList updatedTermNames,
125+
Just referent <- [Map.lookup name (BiMultimap.range unconflictedView.defns.terms)],
126+
Just refId <- [Referent.toTermReferenceId referent]
127+
]
128+
129+
-- Fetch the old terms from the codebase
130+
oldTerms <- Cli.runTransaction do
131+
let refIdSet = Set.fromList (Map.elems updatedTermRefIds)
132+
hydratedTerms <- hydrateRefs env.codebase (Defns refIdSet Set.empty)
133+
pure hydratedTerms.terms
134+
135+
-- Intersect old and new terms to find updated definitions
136+
-- Only include terms where the reference ID actually changed
137+
let updatedTerms :: Map Name ((Term Symbol Ann, Type Symbol Ann), (Term Symbol Ann, Type Symbol Ann))
138+
updatedTerms =
139+
Map.mapMaybe id $
140+
Map.intersectionWith
141+
( \oldRefId (newRefId, newTerm, newTyp) ->
142+
-- Skip terms where the hash hasn't changed (they're not actually updated)
143+
if oldRefId == newRefId
144+
then Nothing
145+
else case Map.lookup oldRefId oldTerms of
146+
Just oldTerm -> Just (oldTerm, (newTerm, newTyp))
147+
Nothing -> Nothing
148+
)
149+
updatedTermRefIds
150+
updatedFileTerms
151+
152+
-- Get type declarations from the file (including reference IDs)
153+
let fileDataDecls :: Map Name (DeclOrBuiltin Symbol Ann)
154+
fileDataDecls =
155+
Map.fromList
156+
[ (Name.unsafeParseVar var, NotBuiltin (Right decl))
157+
| (var, (_, decl)) <- Map.toList (UF.dataDeclarationsId' tuf)
158+
]
159+
160+
let fileEffectDecls :: Map Name (DeclOrBuiltin Symbol Ann)
161+
fileEffectDecls =
162+
Map.fromList
163+
[ (Name.unsafeParseVar var, NotBuiltin (Left decl))
164+
| (var, (_, decl)) <- Map.toList (UF.effectDeclarationsId' tuf)
165+
]
166+
167+
let fileTypeDecls :: Map Name (DeclOrBuiltin Symbol Ann)
168+
fileTypeDecls = Map.union fileDataDecls fileEffectDecls
169+
170+
-- File types with their reference IDs (for updated types rendering)
171+
let fileTypeDeclsWithRefIds :: Map Name (TypeReferenceId, Decl Symbol Ann)
172+
fileTypeDeclsWithRefIds =
173+
Map.fromList $
174+
[ (Name.unsafeParseVar var, (refId, Right decl))
175+
| (var, (refId, decl)) <- Map.toList (UF.dataDeclarationsId' tuf)
176+
]
177+
++ [ (Name.unsafeParseVar var, (refId, Left decl))
178+
| (var, (refId, decl)) <- Map.toList (UF.effectDeclarationsId' tuf)
179+
]
180+
181+
let newTypes :: Map Name (DeclOrBuiltin Symbol Ann)
182+
newTypes = Map.restrictKeys fileTypeDecls newTypeNames
183+
184+
-- Types from the file that are updates to existing codebase definitions
185+
let updatedFileTypes :: Map Name (TypeReferenceId, Decl Symbol Ann)
186+
updatedFileTypes = Map.restrictKeys fileTypeDeclsWithRefIds updatedTypeNames
187+
188+
-- Get the old types from the codebase for updated definitions
189+
-- First, get the type reference IDs for the updated names
190+
let updatedTypeRefIds :: Map Name TypeReferenceId
191+
updatedTypeRefIds =
192+
Map.fromList
193+
[ (name, refId)
194+
| name <- Set.toList updatedTypeNames,
195+
Just typeRef <- [Map.lookup name (BiMultimap.range unconflictedView.defns.types)],
196+
Just refId <- [Reference.toId typeRef]
197+
]
198+
199+
-- Fetch the old types from the codebase
200+
oldTypes <- Cli.runTransaction do
201+
let refIdSet = Set.fromList (Map.elems updatedTypeRefIds)
202+
hydratedTypes <- hydrateRefs env.codebase (Defns Set.empty refIdSet)
203+
pure hydratedTypes.types
204+
205+
-- Intersect old and new types to find updated definitions
206+
-- Only include types where the reference ID actually changed
207+
-- Result: Map Name ((old refId, old decl), (new refId, new decl))
208+
let updatedTypes :: Map Name ((TypeReferenceId, Decl Symbol Ann), (TypeReferenceId, Decl Symbol Ann))
209+
updatedTypes =
210+
Map.mapMaybe id $
211+
Map.intersectionWith
212+
( \oldRefId (newRefId, newDecl) ->
213+
-- Skip types where the hash hasn't changed (they're not actually updated)
214+
if oldRefId == newRefId
215+
then Nothing
216+
else case Map.lookup oldRefId oldTypes of
217+
Just oldDecl -> Just ((oldRefId, oldDecl), (newRefId, newDecl))
218+
Nothing -> Nothing
219+
)
220+
updatedTypeRefIds
221+
updatedFileTypes
222+
223+
-- Build the PPEs:
224+
-- - ppedNew: for new definitions (file names shadowing namespace names)
225+
-- - ppedOld: for old definitions (just namespace names, so old refs resolve properly)
226+
let fileNames = UF.typecheckedToNames tuf
227+
let allNames = fileNames `Names.shadowing` namesIncludingLibdeps
228+
let ppedNew =
229+
PPED.makePPED
230+
(PPE.hqNamer 10 allNames)
231+
(PPE.suffixifyByHash allNames)
232+
let ppedOld =
233+
PPED.makePPED
234+
(PPE.hqNamer 10 namesIncludingLibdeps)
235+
(PPE.suffixifyByHash namesIncludingLibdeps)
236+
237+
-- Respond with the diff
238+
Cli.respond $
239+
Output.ShowUpdateDiff
240+
ppedNew
241+
ppedOld
242+
Defns {terms = newTerms, types = newTypes}
243+
Defns {terms = updatedTerms, types = updatedTypes}
244+
dependents

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ data Input
154154
| DeleteProjectI ProjectName
155155
| DiffBranchI !DiffBranchArg !DiffBranchArg
156156
| DiffNamespaceI BranchId2 BranchId2 -- old new
157+
| DiffUpdateI
157158
| DisplayI OutputLocation (NonEmpty (HQ.HashQualified Name))
158159
| DocToMarkdownI Name
159160
| DocsI (NonEmpty Name)

0 commit comments

Comments
 (0)