Skip to content

Commit c1d143d

Browse files
authored
feat: errors for nested projects (#607)
This PR makes the following adjustments: - Fix a potential bug where nested workspace folders could lead to nested language servers (i.e. in core) - Improve error reporting for nested language servers - Improve error reporting for the 'Restart File' command
1 parent ccbfe24 commit c1d143d

File tree

7 files changed

+166
-71
lines changed

7 files changed

+166
-71
lines changed

vscode-lean4/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,13 +231,13 @@
231231
"command": "lean4.restartServer",
232232
"category": "Lean 4",
233233
"title": "Server: Restart Server",
234-
"description": "Restart the Lean server (for all files)."
234+
"description": "Restarts the Lean server (for all files)."
235235
},
236236
{
237237
"command": "lean4.stopServer",
238238
"category": "Lean 4",
239239
"title": "Server: Stop Server",
240-
"description": "Stop the Lean server (for all files)."
240+
"description": "Stops the Lean server (for all files)."
241241
},
242242
{
243243
"command": "lean4.restartFile",

vscode-lean4/src/diagnostics/setupDiagnostics.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import path from 'path'
12
import { SemVer } from 'semver'
23
import { OutputChannel, commands } from 'vscode'
34
import { ExtUri, FileUri, extUriToCwdUri } from '../utils/exturi'
5+
import { displayInternalError } from '../utils/internalErrors'
46
import { ToolchainUpdateMode } from '../utils/leanCmdRunner'
57
import { LeanInstaller } from '../utils/leanInstaller'
68
import { diagnose } from './setupDiagnoser'
@@ -32,6 +34,22 @@ const ancientLean4ProjectWarningMessage = (origin: string, projectVersion: SemVe
3234
`${origin} is using a Lean 4 version (${projectVersion.toString()}) from before the first Lean 4 stable release (4.0.0).
3335
Pre-stable Lean 4 versions are increasingly less supported, so please consider updating to a newer Lean 4 version.`
3436

37+
const oldServerFolderContainsNewServerFolderErrorMessage = (
38+
folderUri: FileUri,
39+
fileUri: FileUri,
40+
clientFolderUri: FileUri,
41+
) =>
42+
`Error while starting language server: The project at '${folderUri.fsPath}' of the file '${path.basename(fileUri.fsPath)}' is contained inside of another project at '${clientFolderUri.fsPath}', for which a language server is already running.
43+
The Lean 4 VS Code extension does not support nested Lean projects.`
44+
45+
const newServerFolderContainsOldServerFolderErrorMessage = (
46+
folderUri: FileUri,
47+
fileUri: FileUri,
48+
clientFolderUri: FileUri,
49+
) =>
50+
`Error while starting language server: The project at '${folderUri.fsPath}' of the file '${path.basename(fileUri.fsPath)}' contains another project at '${clientFolderUri.fsPath}', for which a language server is already running.
51+
The Lean 4 VS Code extension does not support nested Lean projects.`
52+
3553
export class SetupDiagnostics {
3654
private n: SetupNotifier
3755

@@ -163,6 +181,56 @@ export class SetupDiagnostics {
163181
}
164182
}
165183

184+
async checkIsNestedProjectFolder(
185+
existingFolderUris: ExtUri[],
186+
folderUri: ExtUri,
187+
fileUri: ExtUri,
188+
stopOtherServer: (folderUri: FileUri) => Promise<void>,
189+
): Promise<PreconditionCheckResult> {
190+
if (folderUri.scheme === 'untitled' || fileUri.scheme === 'untitled') {
191+
if (existingFolderUris.some(existingFolderUri => existingFolderUri.scheme === 'untitled')) {
192+
await displayInternalError(
193+
'starting language server',
194+
'Attempting to start new untitled language server while one already exists.',
195+
)
196+
return 'Fatal'
197+
}
198+
return 'Fulfilled'
199+
}
200+
201+
for (const existingFolderUri of existingFolderUris) {
202+
if (existingFolderUri.scheme !== 'file') {
203+
continue
204+
}
205+
if (existingFolderUri.isInFolder(folderUri)) {
206+
return await this.n.displaySetupErrorWithInput(
207+
newServerFolderContainsOldServerFolderErrorMessage(folderUri, fileUri, existingFolderUri),
208+
[
209+
{
210+
input: 'Stop Other Server',
211+
continueDisplaying: false,
212+
action: () => stopOtherServer(existingFolderUri),
213+
},
214+
],
215+
)
216+
}
217+
if (folderUri.isInFolder(existingFolderUri)) {
218+
return await this.n.displaySetupErrorWithInput(
219+
oldServerFolderContainsNewServerFolderErrorMessage(folderUri, fileUri, existingFolderUri),
220+
[
221+
{
222+
input: 'Stop Other Server',
223+
continueDisplaying: false,
224+
action: () => stopOtherServer(existingFolderUri),
225+
},
226+
],
227+
)
228+
}
229+
}
230+
231+
return 'Fulfilled'
232+
}
233+
166234
async checkIsLeanVersionUpToDate(
167235
channel: OutputChannel,
168236
context: string,

vscode-lean4/src/leanclient.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -471,14 +471,32 @@ export class LeanClient implements Disposable {
471471
}
472472

473473
async restartFile(leanDoc: LeanDocument): Promise<void> {
474-
if (this.client === undefined || !this.running) return // there was a problem starting lean server.
474+
const extUri = leanDoc.extUri
475+
const formattedFileName = extUri.scheme === 'file' ? path.basename(extUri.fsPath) : extUri.toString()
476+
const formattedProjectName =
477+
this.folderUri.scheme === 'file' ? this.folderUri.fsPath : this.folderUri.toString()
478+
if (this.client === undefined || !this.running) {
479+
displayNotification(
480+
'Error',
481+
`Cannot restart '${formattedFileName}': The language server for the project at '${formattedProjectName}' is stopped.`,
482+
)
483+
return
484+
}
475485

476-
if (!this.isInFolderManagedByThisClient(leanDoc.extUri)) {
486+
if (!this.isInFolderManagedByThisClient(extUri)) {
487+
displayNotification(
488+
'Error',
489+
`Cannot restart '${formattedFileName}': The project at '${formattedProjectName}' does not contain the file.`,
490+
)
477491
return
478492
}
479493

480-
const uri = leanDoc.extUri.toString()
494+
const uri = extUri.toString()
481495
if (!this.openServerDocuments.delete(uri)) {
496+
displayNotification(
497+
'Error',
498+
`Cannot restart '${formattedFileName}': The file has never been opened in the language server for the project at '${formattedProjectName}'.`,
499+
)
482500
return
483501
}
484502
logger.log(`[LeanClient] Restarting File: ${uri}`)
@@ -488,6 +506,10 @@ export class LeanClient implements Disposable {
488506
)
489507

490508
if (this.openServerDocuments.has(uri)) {
509+
displayNotification(
510+
'Error',
511+
`Cannot restart '${formattedFileName}': The file has already been opened in the language server for the project at '${formattedProjectName}' since initiating the restart.`,
512+
)
491513
return
492514
}
493515
this.openServerDocuments.add(uri)

vscode-lean4/src/utils/clientProvider.ts

Lines changed: 43 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { LeanFileProgressProcessingInfo, ServerStoppedReason } from '@leanprover/infoview-api'
22
import path from 'path'
3-
import { Disposable, EventEmitter, OutputChannel, commands, workspace } from 'vscode'
3+
import { Disposable, EventEmitter, OutputChannel, commands } from 'vscode'
44
import { SetupDiagnostics, checkAll } from '../diagnostics/setupDiagnostics'
55
import { PreconditionCheckResult, SetupNotificationOptions } from '../diagnostics/setupNotifs'
66
import { LeanClient } from '../leanclient'
77
import { LeanPublishDiagnosticsParams } from './converters'
8-
import { ExtUri, FileUri, UntitledUri, getWorkspaceFolderUri } from './exturi'
8+
import { ExtUri, FileUri, UntitledUri } from './exturi'
99
import { lean } from './leanEditorProvider'
1010
import { LeanInstaller } from './leanInstaller'
1111
import { logger } from './logger'
@@ -15,7 +15,10 @@ import { findLeanProjectRoot, willUseLakeServer } from './projectInfo'
1515
async function checkLean4ProjectPreconditions(
1616
channel: OutputChannel,
1717
context: string,
18+
existingFolderUris: ExtUri[],
1819
folderUri: ExtUri,
20+
fileUri: ExtUri,
21+
stopOtherServer: (folderUri: FileUri) => Promise<void>,
1922
): Promise<PreconditionCheckResult> {
2023
const options: SetupNotificationOptions = {
2124
errorMode: { mode: 'NonModal' },
@@ -33,6 +36,7 @@ async function checkLean4ProjectPreconditions(
3336
toolchainUpdateMode: 'PromptAboutUpdate',
3437
})
3538
},
39+
() => d.checkIsNestedProjectFolder(existingFolderUris, folderUri, fileUri, stopOtherServer),
3640
)
3741
}
3842

@@ -84,29 +88,10 @@ export class LeanClientProvider implements Disposable {
8488
commands.registerCommand('lean4.restartFile', () => this.restartActiveFile()),
8589
commands.registerCommand('lean4.refreshFileDependencies', () => this.restartActiveFile()),
8690
commands.registerCommand('lean4.restartServer', () => this.restartActiveClient()),
87-
commands.registerCommand('lean4.stopServer', () => this.stopActiveClient()),
91+
commands.registerCommand('lean4.stopServer', () => this.stopClient(undefined)),
8892
)
8993

9094
this.subscriptions.push(lean.onDidOpenLeanDocument(document => this.ensureClient(document.extUri)))
91-
92-
this.subscriptions.push(
93-
workspace.onDidChangeWorkspaceFolders(event => {
94-
// Remove all clients that are not referenced by any folder anymore
95-
if (event.removed.length === 0) {
96-
return
97-
}
98-
this.clients.forEach((client, key) => {
99-
if (client.folderUri.scheme === 'untitled' || getWorkspaceFolderUri(client.folderUri)) {
100-
return
101-
}
102-
103-
logger.log(`[ClientProvider] onDidChangeWorkspaceFolders removing client for ${key}`)
104-
this.clients.delete(key)
105-
client.dispose()
106-
this.clientRemovedEmitter.fire(client)
107-
})
108-
}),
109-
)
11095
}
11196

11297
getActiveClient(): LeanClient | undefined {
@@ -115,8 +100,6 @@ export class LeanClientProvider implements Disposable {
115100
}
116101

117102
private async onInstallChanged(uri: FileUri) {
118-
// Uri is a package Uri in the case a lean package file was changed.
119-
logger.log(`[ClientProvider] installChanged for ${uri}`)
120103
this.pendingInstallChanged.push(uri)
121104
if (this.processingInstallChanged) {
122105
// avoid re-entrancy.
@@ -135,18 +118,9 @@ export class LeanClientProvider implements Disposable {
135118
continue
136119
}
137120

138-
const preconditionCheckResult = await checkLean4ProjectPreconditions(
139-
this.outputChannel,
140-
'Client Restart',
141-
projectUri,
142-
)
143-
if (preconditionCheckResult !== 'Fatal') {
144-
logger.log('[ClientProvider] got lean version 4')
145-
const [cached, client] = await this.ensureClient(uri)
146-
if (cached && client) {
147-
await client.restart()
148-
logger.log('[ClientProvider] restart complete')
149-
}
121+
const [cached, client] = await this.ensureClient(uri)
122+
if (cached && client) {
123+
await client.restart()
150124
}
151125
} catch (e) {
152126
logger.log(`[ClientProvider] Exception checking lean version: ${e}`)
@@ -185,9 +159,33 @@ export class LeanClientProvider implements Disposable {
185159
this.restartFile(doc.extUri)
186160
}
187161

188-
private async stopActiveClient() {
189-
if (this.activeClient && this.activeClient.isStarted()) {
190-
await this.activeClient?.stop()
162+
private async stopClient(folderUri: ExtUri | undefined) {
163+
let clientToStop: LeanClient
164+
if (folderUri === undefined) {
165+
if (this.activeClient === undefined) {
166+
displayNotification('Error', 'Cannot stop language server: No active client.')
167+
return
168+
}
169+
clientToStop = this.activeClient
170+
} else {
171+
const foundClient = this.getClientForFolder(folderUri)
172+
if (foundClient === undefined) {
173+
displayNotification(
174+
'Error',
175+
`Cannot stop language server: No client for project at '${folderUri.toString()}'.`,
176+
)
177+
return
178+
}
179+
clientToStop = foundClient
180+
}
181+
if (clientToStop.isStarted()) {
182+
await clientToStop.stop()
183+
}
184+
const key = clientToStop.folderUri.toString()
185+
this.clients.delete(key)
186+
this.pending.delete(key)
187+
if (clientToStop === this.activeClient) {
188+
this.activeClient = undefined
191189
}
192190
}
193191

@@ -264,7 +262,13 @@ export class LeanClientProvider implements Disposable {
264262
const preconditionCheckResult = await checkLean4ProjectPreconditions(
265263
this.outputChannel,
266264
'Client Startup',
265+
this.getClients().map(client => client.folderUri),
267266
folderUri,
267+
uri,
268+
async (folderUriToStop: FileUri) => {
269+
await this.stopClient(folderUriToStop)
270+
await this.ensureClient(uri)
271+
},
268272
)
269273
if (preconditionCheckResult === 'Fatal') {
270274
this.pending.delete(key)

vscode-lean4/src/utils/exturi.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,15 @@ export class FileUri {
6666
}
6767
}
6868

69-
export function getWorkspaceFolderUri(uri: FileUri): FileUri | undefined {
70-
const folder = workspace.getWorkspaceFolder(uri.asUri())
71-
if (folder === undefined) {
72-
return undefined
73-
}
74-
const folderUri = FileUri.fromUri(folder.uri)
75-
if (folderUri === undefined) {
76-
return undefined
69+
export function isInWorkspaceFolder(uri: FileUri): boolean {
70+
return workspace.getWorkspaceFolder(uri.asUri()) !== undefined
71+
}
72+
73+
export function isWorkspaceFolder(uri: FileUri): boolean {
74+
if (workspace.workspaceFolders === undefined) {
75+
return false
7776
}
78-
return folderUri
77+
return workspace.workspaceFolders.some(folder => uri.equalsUri(folder.uri))
7978
}
8079

8180
export class UntitledUri {
Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,27 @@
11
import { env } from 'vscode'
22
import { displayNotificationWithInput } from './notifs'
33

4+
export async function displayInternalError(scope: string, e: any) {
5+
let msg: string = `Internal error (while ${scope}): ${e}`
6+
let fullMsg: string = msg
7+
if (e instanceof Error && e.stack !== undefined) {
8+
fullMsg += `\n\n${e.stack}`
9+
}
10+
msg +=
11+
"\n\nIf you are using an up-to-date version of the Lean 4 VS Code extension, please copy the full error message using the 'Copy Error to Clipboard' button and report it at https://github.com/leanprover/vscode-lean4/ or https://leanprover.zulipchat.com/."
12+
const copyToClipboardInput = 'Copy Error to Clipboard'
13+
const closeInput = 'Close'
14+
const choice = await displayNotificationWithInput('Error', msg, [copyToClipboardInput], closeInput)
15+
if (choice === copyToClipboardInput) {
16+
await env.clipboard.writeText(fullMsg)
17+
}
18+
}
19+
420
export async function displayInternalErrorsIn<T>(scope: string, f: () => Promise<T>): Promise<T> {
521
try {
622
return await f()
723
} catch (e) {
8-
let msg: string = `Internal error (while ${scope}): ${e}`
9-
let fullMsg: string = msg
10-
if (e instanceof Error && e.stack !== undefined) {
11-
fullMsg += `\n\n${e.stack}`
12-
}
13-
msg +=
14-
"\n\nIf you are using an up-to-date version of the Lean 4 VS Code extension, please copy the full error message using the 'Copy Error to Clipboard' button and report it at https://github.com/leanprover/vscode-lean4/ or https://leanprover.zulipchat.com/."
15-
const copyToClipboardInput = 'Copy Error to Clipboard'
16-
const closeInput = 'Close'
17-
const choice = await displayNotificationWithInput('Error', msg, [copyToClipboardInput], closeInput)
18-
if (choice === copyToClipboardInput) {
19-
await env.clipboard.writeText(fullMsg)
20-
}
24+
await displayInternalError(scope, e)
2125
throw e
2226
}
2327
}

vscode-lean4/src/utils/projectInfo.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as fs from 'fs'
2-
import { ExtUri, FileUri, getWorkspaceFolderUri } from './exturi'
3-
import { dirExists, fileExists } from './fsHelper'
42
import path from 'path'
3+
import { ExtUri, FileUri, isWorkspaceFolder } from './exturi'
4+
import { dirExists, fileExists } from './fsHelper'
55

66
// Detect lean4 root directory (works for both lean4 repo and nightly distribution)
77

@@ -42,8 +42,6 @@ export async function findLeanProjectRootInfo(uri: FileUri): Promise<ProjectRoot
4242
const toolchainFileName = 'lean-toolchain'
4343

4444
let path = uri
45-
const containingWsFolderUri = getWorkspaceFolderUri(uri)
46-
4745
try {
4846
if ((await fs.promises.stat(path.fsPath)).isFile()) {
4947
path = uri.join('..')
@@ -65,7 +63,7 @@ export async function findLeanProjectRootInfo(uri: FileUri): Promise<ProjectRoot
6563
// Stop searching in case users accidentally created a lean-toolchain file above the core directory
6664
break
6765
}
68-
if (containingWsFolderUri !== undefined && path.equals(containingWsFolderUri)) {
66+
if (isWorkspaceFolder(path)) {
6967
if (bestLeanToolchain === undefined) {
7068
// If we haven't found a toolchain yet, prefer the workspace folder as the project scope for the file,
7169
// but keep looking in case there is a lean-toolchain above the workspace folder

0 commit comments

Comments
 (0)