@@ -11,6 +11,7 @@ import Lean.Data.Lsp.Utf16
1111import Lean.Message
1212import Lean.Meta.TryThis
1313import Lean.Util.Diff
14+ import Lean.Util.EditDistance
1415import Lean.Widget.Types
1516import Lean.PrettyPrinter
1617
@@ -66,48 +67,43 @@ export default function ({ diff, range, suggestion }) {
6667 e('span', defStyle, comp.text)
6768 )
6869 const fullDiff = e('span',
69- { onClick, title: 'Apply suggestion', className: 'link pointer dim font-code', },
70+ { onClick,
71+ title: 'Apply suggestion',
72+ className: 'link pointer dim font-code',
73+ style: { display: 'inline-block', verticalAlign: 'text-top' } },
7074 spans)
7175 return fullDiff
7276}"
7377
7478/--
7579Converts an array of diff actions into corresponding JSON interpretable by `tryThisDiffWidget`.
7680-/
77- private def mkDiffJson (ds : Array (Diff.Action × Char)) :=
78- -- Avoid cluttering the DOM by grouping "runs" of the same action
79- let unified : List (Diff.Action × List Char) := ds.foldr (init := []) fun
80- | (act, c), [] => [(act, [c])]
81- | (act, c), (act', cs) :: acc =>
82- if act == act' then
83- (act, c :: cs) :: acc
84- else
85- (act, [c]) :: (act', cs) :: acc
86- toJson <| unified.map fun
87- | (.insert, s) => json% { type: "insertion" , text: $(String.mk s) }
88- | (.delete, s) => json% { type: "deletion" , text: $(String.mk s) }
89- | (.skip , s) => json% { type: "unchanged" , text: $(String.mk s) }
81+ private def mkDiffJson (ds : Array (Diff.Action × String)) :=
82+ toJson <| ds.map fun
83+ | (.insert, s) => json% { type: "insertion" , text: $s }
84+ | (.delete, s) => json% { type: "deletion" , text: $s }
85+ | (.skip , s) => json% { type: "unchanged" , text: $s }
9086
9187/--
9288Converts an array of diff actions into a Unicode string that visually depicts the diff.
9389
9490Note that this function does not return the string that results from applying the diff to some
95- input; rather, it returns a string representation of the actions that the diff itself comprises, such as `b̵a̵c̲h̲e̲e̲rs̲`.
96-
91+ input; rather, it returns a string representation of the actions that the diff itself comprises,
92+ such as `b̵a̵c̲h̲e̲e̲rs̲`.
9793-/
98- private def mkDiffString (ds : Array (Diff.Action × Char )) : String :=
94+ private def mkDiffString (ds : Array (Diff.Action × String )) : String :=
9995 let rangeStrs := ds.map fun
100- | (.insert, s) => String.mk [s , '\u0332 ' ] -- U+0332 Combining Low Line
101- | (.delete, s) => String.mk [s , '\u0335 ' ] -- U+0335 Combining Short Stroke Overlay
102- | (.skip , s) => String.mk [s]
96+ | (.insert, s) => String.mk (s.data.flatMap ([· , '\u0332 ' ])) -- U+0332 Combining Low Line
97+ | (.delete, s) => String.mk (s.data.flatMap ([· , '\u0335 ' ])) -- U+0335 Combining Short Stroke Overlay
98+ | (.skip , s) => s
10399 rangeStrs.foldl (· ++ ·) ""
104100
105101/--
106102A code action suggestion associated with a hint in a message.
107103
108104Refer to `TryThis.Suggestion`; this extends that structure with a `span?` field, allowing a single
109- hint to suggest modifications at different locations. If `span?` is not specified, then the `ref`
110- for the containing `Suggestions` value is used.
105+ hint to suggest modifications at different locations. If `span?` is not specified, then the syntax
106+ reference provided to `MessageData.hint` will be used.
111107-/
112108structure Suggestion extends TryThis.Suggestion where
113109 span? : Option Syntax := none
@@ -119,40 +115,83 @@ instance : ToMessageData Suggestion where
119115 toMessageData s := toMessageData s.toSuggestion
120116
121117/--
122- A collection of code action suggestions to be included in a hint in a diagnostic message.
118+ Produces a diff that splits either on characters, tokens, or not at all, depending on the edit
119+ distance between the arguments.
123120
124- Contains the following fields:
125- * `ref`: the syntax location for the code action suggestions. Will be overridden by the `span?`
126- field on any suggestions that specify it.
127- * `suggestions`: the suggestions to display.
128- * `codeActionPrefix?`: if specified, text to display in place of "Try this: " in the code action
129- label
121+ Guarantees that all actions in the output will be maximally grouped; that is, instead of returning
122+ `#[(.insert, "a"), (.insert, "b")]`, it will return `#[(.insert, "ab")]`.
130123-/
131- structure Suggestions where
132- ref : Syntax
133- suggestions : Array Suggestion
134- codeActionPrefix? : Option String := none
124+ def readableDiff (s s' : String) : Array (Diff.Action × String) :=
125+ let minLength := min s.length s'.length
126+ -- Given that `Diff.diff` returns a minimal diff, any length-≤3 diff can only have edits at the
127+ -- front and back, or at a single interior point. This will always be fairly readable, so we can
128+ -- return it without further analysis:
129+ if charDiff.size ≤ 3 then
130+ charDiff
131+ else
132+ -- The coefficients on these two constants can be tuned:
133+ let charDiffDistance := minLength / 5
134+ let wordDiffDistance := minLength
135+ if let some editDistance := EditDistance.levenshtein s s' wordDiffDistance then
136+ if editDistance < charDiffDistance then
137+ charDiff
138+ else
139+ wordDiff
140+ else
141+ maxDiff
142+ where
143+ splitChars (s : String) :=
144+ s.toList.toArray
145+
146+ splitWords (s : String) :=
147+ s.splitOn " " |>.intersperse " " |>.toArray
148+
149+ charDiff :=
150+ Diff.diff (splitChars s) (splitChars s')
151+ |> joinEdits
152+ |>.map fun (act, cs) => (act, String.mk cs.toList)
153+
154+ wordDiff :=
155+ Diff.diff (splitWords s) (splitWords s')
156+ |> joinEdits
157+ |>.map fun (act, ss) => (act, ss.foldl (· ++ ·) "" )
158+
159+ maxDiff :=
160+ #[(.delete, s), (.insert, s')]
161+
162+ joinEdits {α} (ds : Array (Diff.Action × α)) : Array (Diff.Action × Array α) :=
163+ ds.foldl (init := #[]) fun acc (act, c) =>
164+ if h : acc.isEmpty then
165+ #[(act, #[c])]
166+ else
167+ have : acc.size - 1 < acc.size := Nat.sub_one_lt <| mt Array.size_eq_zero_iff.mp <|
168+ Array.isEmpty_eq_false_iff.mp (Bool.of_not_eq_true h)
169+ let (act', cs) := acc[acc.size - 1 ]
170+ if act == act' then
171+ acc.set (acc.size - 1 ) (act, cs.push c)
172+ else
173+ acc.push (act, #[c])
135174
136175/--
137176Creates message data corresponding to a `HintSuggestions` collection and adds the corresponding info
138177leaf.
139178-/
140- def Suggestions.toHintMessage (suggestions : Suggestions) : CoreM MessageData := do
141- let { ref, codeActionPrefix?, suggestions } := suggestions
179+ def mkSuggestionsMessage (suggestions : Array Suggestion)
180+ (ref : Syntax)
181+ (codeActionPrefix? : Option String) : CoreM MessageData := do
142182 let mut msg := m!""
143183 for suggestion in suggestions do
144184 if let some range := (suggestion.span?.getD ref).getRange? then
145- let { info, suggestions := suggestionArr, range := lspRange } ← processSuggestions ref range
146- #[suggestion.toSuggestion] codeActionPrefix?
185+ let { info, suggestions := suggestionArr, range := lspRange } ←
186+ processSuggestions ref range #[suggestion.toSuggestion] codeActionPrefix?
147187 pushInfoLeaf info
148188 let suggestionText := suggestionArr[0 ]!.2 .1
149189 let map ← getFileMap
150190 let rangeContents := Substring.mk map.source range.start range.stop |>.toString
151- let split (s : String) := s.toList.toArray
152- let edits := Diff.diff (split rangeContents) (split suggestionText)
153- let diff := mkDiffJson edits
191+ let edits := readableDiff rangeContents suggestionText
192+ let diffJson := mkDiffJson edits
154193 let json := json% {
155- diff: $diff ,
194+ diff: $diffJson ,
156195 suggestion: $suggestionText,
157196 range: $lspRange
158197 }
@@ -167,15 +206,28 @@ def Suggestions.toHintMessage (suggestions : Suggestions) : CoreM MessageData :=
167206 let suggestionMsg := if suggestions.size == 1 then
168207 MessageData.ofFormat "\n " ++ m!"{widgetMsg}"
169208 else
170- MessageData.ofFormat "\n " ++ m!"• {widgetMsg}"
209+ MessageData.ofFormat "\n " ++ MessageData.nest 2 m!"• {widgetMsg}"
171210 msg := msg ++ MessageData.nestD suggestionMsg
172211 return msg
173212
174213/--
175- Creates a hint message from `hint` with associated code action(s) given by ` suggestions` .
214+ Creates a hint message with associated code action suggestions.
176215
177216To provide a hint without an associated code action, use `MessageData.hint'`.
217+
218+ The arguments are as follows:
219+ * `hint`: the main message of the hint, which precedes its code action suggestions.
220+ * `suggestions`: the suggestions to display.
221+ * `ref?`: if specified, the syntax location for the code action suggestions; otherwise, default to
222+ the syntax reference in the monadic state. Will be overridden by the `span?` field on any
223+ suggestions that specify it.
224+ * `codeActionPrefix?`: if specified, text to display in place of "Try this: " in the code action
225+ label
178226 -/
179- def _root_.Lean.MessageData.hint (hint : MessageData) (suggestions : Suggestions)
227+ def _root_.Lean.MessageData.hint (hint : MessageData)
228+ (suggestions : Array Suggestion) (ref? : Option Syntax := none)
229+ (codeActionPrefix? : Option String := none)
180230 : CoreM MessageData := do
181- return .tagged `hint (.ofFormat "\n\n Hint: " ++ hint ++ (← suggestions.toHintMessage))
231+ let ref := ref?.getD (← getRef)
232+ let suggs ← mkSuggestionsMessage suggestions ref codeActionPrefix?
233+ return .tagged `hint (m!"\n\n Hint: " ++ hint ++ suggs)
0 commit comments