Skip to content

Commit 0773e20

Browse files
committed
Fix splitting terms with inner colons in our interactive hovers.
This fixes the example from the issue and is a bit more robust, though this specific feature may still be more trouble than it's worth without actually writing the RFC for upstream support. Closes: #521
1 parent bcb08ba commit 0773e20

2 files changed

Lines changed: 78 additions & 6 deletions

File tree

lua/lean/hover.lua

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,25 @@ local function apply_lean_syntax(buffer, last_line)
5555
end)
5656
end
5757

58+
---Find the byte index of the top-level `:` in a Lean signature, i.e. the
59+
---colon that separates the binder list from the result type. Bracket-aware
60+
---so colons inside binders such as `(x : T)` or `{x : T}` are skipped.
61+
---@param signature string
62+
---@return integer? index 1-based byte index of the `:` character, or nil if none
63+
local function top_level_colon(signature)
64+
local depth = 0
65+
for i = 1, #signature do
66+
local c = signature:sub(i, i)
67+
if c == '(' or c == '{' or c == '[' then
68+
depth = depth + 1
69+
elseif c == ')' or c == '}' or c == ']' then
70+
depth = depth - 1
71+
elseif c == ':' and depth == 0 and signature:sub(i + 1, i + 1) ~= '=' then
72+
return i
73+
end
74+
end
75+
end
76+
5877
---Extract the signature and documentation from a Lean hover result.
5978
---
6079
---Lean's hover markdown has the form:
@@ -68,7 +87,7 @@ end
6887
--- *import Module*
6988
---
7089
---We show the type interactively via RPC, so we extract the expression
71-
---name/signature (everything before the final ` : ` in the code fence)
90+
---name/signature (everything before the top-level `:` in the code fence)
7291
---and the documentation (everything after the first `***` separator).
7392
---@param result table the LSP hover result
7493
---@return string? signature the expression signature (without the type)
@@ -79,12 +98,20 @@ local function extract_hover(result)
7998
return
8099
end
81100

82-
-- Extract the expression name from inside the code fence.
83-
-- The fence contains lines like "Nat.add : Nat → Nat → Nat" or
84-
-- "foo (n : Nat) : Nat". The greedy (.+) finds the last " : ",
85-
-- which separates the expression from its type.
101+
-- Extract the expression name from inside the code fence. The fence
102+
-- contains lines like `Nat.add : Nat → Nat → Nat` or `foo (n : Nat) : Nat`,
103+
-- and for multi-line signatures the top-level `:` can be at end of a line
104+
-- with no trailing space. We walk the fence tracking bracket depth so
105+
-- colons inside binders never split the signature.
86106
local fence = value:match '^```lean\n(.-)```'
87-
local signature = fence and vim.trim(fence):match '(.+) : '
107+
local signature
108+
if fence then
109+
local trimmed = vim.trim(fence)
110+
local colon = top_level_colon(trimmed)
111+
if colon then
112+
signature = vim.trim(trimmed:sub(1, colon - 1))
113+
end
114+
end
88115

89116
-- Extract documentation after the first *** separator. Lean uses `***`
90117
-- as a thematic break; we convert to `---` for nicer rendering, but pad

spec/hover_spec.lua

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ describe(
1616
[[
1717
#check Nat
1818
#check @Nat.add
19+
theorem mlt_521_repro {α : Type} {β : Type} (h_a : α = β) (h_b : β = α) (h_c : α = β) (h_d : β = α) :
20+
(∀ x : α, x = x) ∧ (∀ y : β, y = y) ∧ (∀ z : α, z = z) ∧ (∀ w : β, w = w) :=
21+
sorry
1922
]],
2023
function()
2124
local lean_window = Window:current()
@@ -148,6 +151,48 @@ describe(
148151
lean_window:make_current()
149152
end)
150153

154+
it('preserves multi-line signatures whose binders contain colons', function()
155+
lean_window:make_current()
156+
-- Hover on the theorem name in its declaration on line 3.
157+
helpers.move_cursor { to = { 3, 10 } }
158+
159+
local ids = { lean_window.id, current_infoview.window.id }
160+
161+
hover()
162+
-- Poll until the hover window actually contains our theorem signature;
163+
-- the interactive-hover RPC can take longer than the default 1s
164+
-- `wait_for_new_window` allows.
165+
local hover_win
166+
local ok = vim.wait(15000, function()
167+
hover_win = vim.iter(vim.api.nvim_tabpage_list_wins(0)):find(function(w)
168+
if vim.tbl_contains(ids, w) then
169+
return false
170+
end
171+
local buf = vim.api.nvim_win_get_buf(w)
172+
local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
173+
return table.concat(lines, '\n'):match 'mlt_521_repro' ~= nil
174+
end)
175+
return hover_win ~= nil
176+
end)
177+
assert.message('Interactive hover never rendered.').is_true(ok)
178+
hover_win = Window:from_id(hover_win)
179+
180+
local contents = table.concat(hover_win:buffer():lines(), '\n')
181+
182+
-- Regression for #521: the parser used to split the signature at a
183+
-- ` : ` inside a binder when Lean wrapped the signature so the
184+
-- top-level `:` sat at end-of-line. The final binder `(h_d : β = α)`
185+
-- ended up rendered as `∀ (w : <conclusion type>` instead.
186+
assert.has_match('%(h_d : β = α%)', contents)
187+
assert.is_nil(
188+
contents:match '∀%s*%(w%s*:%s*∀',
189+
('binder `w` was misparsed:\n%s'):format(contents)
190+
)
191+
192+
hover_win:close()
193+
lean_window:make_current()
194+
end)
195+
151196
it('falls back to standard hover when RPC fails', function()
152197
lean_window:make_current()
153198
helpers.move_cursor { to = { 1, 0 } }

0 commit comments

Comments
 (0)