Skip to content

Commit 90eb615

Browse files
authored
feat: editor decorations (#585)
This PR adds several new editor decorations. ### Error and warning gutter decorations To resolve the issue that warnings and errors can sometimes be a bit difficult to spot, especially when the corresponding diagnostic squiggly is small, this PR adds a system of gutter decorations to make errors and warnings much more visible. ![Error and warning gutter decorations](https://github.com/user-attachments/assets/a611e2ea-aa26-4c1f-b945-8e86dfbeef4c) The circled error and warning symbols denote the line where an error or a warning starts, i.e. where the squiggly is displayed. A red vertical line denotes the *full range* of the error, i.e. the range in which the error is displayed under 'Messages' in the InfoView. The red line bends to the right whenever a full error range ends in that line of code. Single line errors are displayed without a red line. These gutter decorations can be disabled using the 'Lean4: Show Diagnostic Gutter Decorations' setting. When the gutter icons of the Error Lens extension are enabled, the gutter decorations of the Lean 4 VS Code extension are disabled. Once this PR is merged, this feature will be available in all Lean 4 versions. ### 'Unsolved goals' markers To resolve the issue that it can sometimes be a bit difficult to tell where one should continue typing in a proof, this PR adds an end-of-line editor decoration that is displayed in the line where the full range of an 'unsolved goals' error ends. ![Unsolved goals markers](https://github.com/user-attachments/assets/32562122-6ce6-490e-a9db-5ae3e8a6e79f) The 'unsolved goals' markers can be disabled using the 'Lean 4: Show Unsolved Goal Decoration' setting. This feature needs language server support, so it will only be active for Lean versions that include the companion PR at leanprover/lean4#7366. ### Goals accomplished! To re-introduce some positive reinforcement into Lean 4 after the 'Goals accomplished!' message from Lean 3 was removed due to often being confusing in Lean 4, this PR adds a new 'Goals accomplished!' mechanism when a theorem or a `Prop`-typed `example` has been proven. ![Goals accomplished](https://github.com/user-attachments/assets/f303d580-cd4a-4697-80e6-d0d1114fd454) When a theorem or a `Prop`-typed `example` contains no errors or `sorry`s anymore, two blue checkmarks appear next to the start of the declaration as a gutter decoration. Additionally, a 'Goals accomplished!' message appears under 'Messages' in the InfoView. The 'Goals accomplished!' message does not appear in 'All Messages' and is not displayed with a squiggly line in the editor. The 'Goals accomplished!' gutter decoration can be configured or disabled using the 'Lean 4: Goals Accomplished Decoration Kind' setting. In addition to the default double checkmark decoration pictured above, this PR adds the following additional styles for the gutter decoration: - Circled checkmark: ![Circled checkmark](https://github.com/user-attachments/assets/0014bf2c-6dee-4af7-9c01-7cd470efa1ca) - Octopus: ![Octopus](https://github.com/user-attachments/assets/9a65ad3b-d032-414c-b632-3afc943d45ae) - Tada: ![Tada](https://github.com/user-attachments/assets/942e3973-444c-4701-b86a-0570ff437cd0) This feature needs language server support, so it will only be active for Lean versions that include the companion PR at leanprover/lean4#7366. ### Technical notes - We intend to test this implementation for the 'unsolved goals' markers and 'Goals accomplished!' in core before v4.19.0-rc1 to iron out any remaining issues. - All gutter decorations are implemented using .svgs, which is the only API that VS Code provides to display things in the gutter. Since there are lots of different cases to consider for the red vertical error range lines, as well as for light and dark themes, this PR ships 56 .svg files in total. - The design language of the red lines for the error range gutter decorations is deliberately not perfectly unambiguous. For example, when multiple full error ranges overlap, it isn't clear which error range is terminated by the line bending to the right. The design in this PR is the best I could come up with for the limited size of the gutter without introducing too much visual complexity or noise. - The task gutter had to be rewritten entirely for this PR, including the progress bar code. - Since the line where the full range of an 'unsolved goals' error ends is relatively unstable as the document is being edited, this PR implements an edit delay of 3000ms for 'unsolved goals' markers, so that they do not constantly jump around while editing the document. - The 'unsolved goals' markers use an emoji (specifically the 'Hammer and wrench' emoji U+1F6E0) instead of an icon. The reason for this is that VS Code's decoration API appears to be pretty broken for end-of-line icons and fails to vertically center them correctly. - None of these decorations have an associated hover to explain the semantics of each decoration. The reason for this is that VS Code doesn't support gutter decoration hovers and for end-of-line decorations, it pollutes the hover of every hover in the whole line with the hover message of the end-of-line decoration. - This PR includes the client-side for the protocol extensions defined at leanprover/lean4#7366.
1 parent c4d12fc commit 90eb615

File tree

73 files changed

+9822
-150
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+9822
-150
lines changed

lean4-infoview-api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@leanprover/infoview-api",
3-
"version": "0.5.0",
3+
"version": "0.6.0",
44
"description": "Types and API for @leanprover/infoview.",
55
"scripts": {
66
"watch": "tsc --watch",

lean4-infoview-api/src/lspTypes.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,30 @@
11
import type {
22
Diagnostic,
33
DocumentUri,
4+
integer,
45
Range,
56
TextDocumentPositionParams,
67
VersionedTextDocumentIdentifier,
78
} from 'vscode-languageserver-protocol'
89

910
// Lean 4 extensions to LSP.
1011

12+
export enum LeanTag {
13+
UnsolvedGoals = 1, // introduced in 2025-03-05
14+
GoalsAccomplished = 2, // introduced in 2025-03-05
15+
}
16+
1117
/** Used in place of {@link Diagnostic} within `textDocument/publishDiagnostics`. */
1218
export interface LeanDiagnostic extends Diagnostic {
1319
fullRange?: Range // introduced in 2021-03-10
20+
isSilent?: boolean // introduced in 2025-03-05
21+
leanTags?: LeanTag[] // introduced in 2025-03-05
22+
}
23+
24+
export interface LeanPublishDiagnosticsParams {
25+
uri: DocumentUri
26+
version?: integer
27+
diagnostics: LeanDiagnostic[]
1428
}
1529

1630
export interface PlainGoal {

lean4-infoview/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@leanprover/infoview",
3-
"version": "0.8.2",
3+
"version": "0.8.3",
44
"description": "An interactive display for the Lean 4 theorem prover.",
55
"scripts": {
66
"watch": "rollup --config --environment NODE_ENV:development --watch",
@@ -49,7 +49,7 @@
4949
"typescript": "^5.4.5"
5050
},
5151
"dependencies": {
52-
"@leanprover/infoview-api": "~0.5.0",
52+
"@leanprover/infoview-api": "~0.6.0",
5353
"@vscode/codicons": "^0.0.32",
5454
"@vscode-elements/react-elements": "^0.5.0",
5555
"es-module-lexer": "^1.5.4",

lean4-infoview/src/infoview/contexts.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import * as React from 'react'
2-
import type { Diagnostic, DocumentUri } from 'vscode-languageserver-protocol'
2+
import type { DocumentUri } from 'vscode-languageserver-protocol'
33

4-
import { defaultInfoviewConfig, InfoviewConfig, LeanFileProgressProcessingInfo } from '@leanprover/infoview-api'
4+
import {
5+
defaultInfoviewConfig,
6+
InfoviewConfig,
7+
LeanDiagnostic,
8+
LeanFileProgressProcessingInfo,
9+
} from '@leanprover/infoview-api'
510

611
import { EditorConnection } from './editorConnection'
712
import { ServerVersion } from './serverVersion'
@@ -13,7 +18,7 @@ export const EditorContext = React.createContext<EditorConnection>(null as any)
1318
export const VersionContext = React.createContext<ServerVersion | undefined>(undefined)
1419

1520
export const ConfigContext = React.createContext<InfoviewConfig>(defaultInfoviewConfig)
16-
export const LspDiagnosticsContext = React.createContext<Map<DocumentUri, Diagnostic[]>>(new Map())
21+
export const LspDiagnosticsContext = React.createContext<Map<DocumentUri, LeanDiagnostic[]>>(new Map())
1722
export const ProgressContext = React.createContext<Map<DocumentUri, LeanFileProgressProcessingInfo[]>>(new Map())
1823

1924
/**

lean4-infoview/src/infoview/info.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react'
2-
import type { Diagnostic, Location } from 'vscode-languageserver-protocol'
2+
import type { Location } from 'vscode-languageserver-protocol'
33

44
import {
55
getInteractiveDiagnostics,
@@ -415,13 +415,13 @@ function InfoAux(props: InfoProps) {
415415
// Compute the LSP diagnostics at this info's position. We try to ensure that if these remain
416416
// the same, then so does the identity of `lspDiagsHere` so that it can be used as a dep.
417417
const lspDiags = React.useContext(LspDiagnosticsContext)
418-
const [lspDiagsHere, setLspDiagsHere] = React.useState<Diagnostic[]>([])
418+
const [lspDiagsHere, setLspDiagsHere] = React.useState<LeanDiagnostic[]>([])
419419
React.useEffect(() => {
420420
// Note: the curly braces are important. https://medium.com/geekculture/react-uncaught-typeerror-destroy-is-not-a-function-192738a6e79b
421421
setLspDiagsHere(diags0 => {
422-
const diagPred = (d: Diagnostic) =>
422+
const diagPred = (d: LeanDiagnostic) =>
423423
RangeHelpers.contains(
424-
(d as LeanDiagnostic).fullRange || d.range,
424+
d.fullRange || d.range,
425425
{ line: pos.line, character: pos.character },
426426
config.allErrorsOnLine,
427427
)

lean4-infoview/src/infoview/messages.tsx

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
11
import * as React from 'react'
22
import fastIsEqual from 'react-fast-compare'
3-
import {
4-
Diagnostic,
5-
DiagnosticSeverity,
6-
DocumentUri,
7-
Location,
8-
PublishDiagnosticsParams,
9-
} from 'vscode-languageserver-protocol'
3+
import { Diagnostic, DiagnosticSeverity, DocumentUri, Location } from 'vscode-languageserver-protocol'
104

11-
import { LeanDiagnostic, RpcErrorCode } from '@leanprover/infoview-api'
5+
import { LeanDiagnostic, LeanPublishDiagnosticsParams, RpcErrorCode } from '@leanprover/infoview-api'
126

137
import { getInteractiveDiagnostics, InteractiveDiagnostic } from '@leanprover/infoview-api'
148
import { Details } from './collapsing'
@@ -135,7 +129,9 @@ export function AllMessages({ uri: uri0 }: { uri: DocumentUri }) {
135129
const rs0 = useRpcSessionAtPos({ uri: uri0, line: 0, character: 0 })
136130
const dc = React.useContext(LspDiagnosticsContext)
137131
const config = React.useContext(ConfigContext)
138-
const diags0 = React.useMemo(() => dc.get(uri0) || [], [dc, uri0])
132+
const diags0 = React.useMemo(() => dc.get(uri0) || [], [dc, uri0]).filter(
133+
diag => diag.isSilent === undefined || !diag.isSilent,
134+
)
139135

140136
const iDiags0 = React.useMemo(
141137
() =>
@@ -145,7 +141,8 @@ export function AllMessages({ uri: uri0 }: { uri: DocumentUri }) {
145141
// ensures that the call doesn't block until the whole file is elaborated.
146142
const maxLine = diags0.reduce((ln, d) => Math.max(ln, d.range.end.line), 0) + 1
147143
try {
148-
const diags = await getInteractiveDiagnostics(rs0, { start: 0, end: maxLine })
144+
let diags = await getInteractiveDiagnostics(rs0, { start: 0, end: maxLine })
145+
diags = diags.filter(d => d.isSilent === undefined || !d.isSilent)
149146
if (diags.length > 0) {
150147
return diags
151148
}
@@ -159,7 +156,7 @@ export function AllMessages({ uri: uri0 }: { uri: DocumentUri }) {
159156
console.log('getInteractiveDiagnostics error ', err)
160157
}
161158
}
162-
return diags0.map(d => ({ ...(d as LeanDiagnostic), message: { text: d.message } }))
159+
return diags0.map(d => ({ ...d, message: { text: d.message } }))
163160
}),
164161
[rs0, diags0],
165162
)
@@ -247,8 +244,8 @@ function AllMessagesBody({ uri, messages, setNumDiags }: AllMessagesBodyProps) {
247244
export function WithLspDiagnosticsContext({ children }: React.PropsWithChildren<{}>) {
248245
const [allDiags, _0] = useServerNotificationState(
249246
'textDocument/publishDiagnostics',
250-
new Map<DocumentUri, Diagnostic[]>(),
251-
async (params: PublishDiagnosticsParams) => diags => new Map(diags).set(params.uri, params.diagnostics),
247+
new Map<DocumentUri, LeanDiagnostic[]>(),
248+
async (params: LeanPublishDiagnosticsParams) => diags => new Map(diags).set(params.uri, params.diagnostics),
252249
[],
253250
)
254251

package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)