From e48afdfa321b77aa047d47d0f2b409c15049161d Mon Sep 17 00:00:00 2001 From: mhuisi Date: Fri, 15 Sep 2023 10:42:46 +0200 Subject: [PATCH 01/18] feat: new commands --- vscode-lean4/media/lean-mini-dark.svg | 259 +++++++++++++++++++++++ vscode-lean4/media/lean-mini-light.svg | 259 +++++++++++++++++++++++ vscode-lean4/package.json | 210 ++++++++++++++++-- vscode-lean4/src/docview.ts | 27 +-- vscode-lean4/src/exports.ts | 16 +- vscode-lean4/src/extension.ts | 19 +- vscode-lean4/src/project.ts | 159 ++++++++++++++ vscode-lean4/src/tasks.ts | 87 ++++++++ vscode-lean4/src/utils/clientProvider.ts | 10 +- 9 files changed, 1005 insertions(+), 41 deletions(-) create mode 100644 vscode-lean4/media/lean-mini-dark.svg create mode 100644 vscode-lean4/media/lean-mini-light.svg create mode 100644 vscode-lean4/src/project.ts create mode 100644 vscode-lean4/src/tasks.ts diff --git a/vscode-lean4/media/lean-mini-dark.svg b/vscode-lean4/media/lean-mini-dark.svg new file mode 100644 index 000000000..5ba4d929f --- /dev/null +++ b/vscode-lean4/media/lean-mini-dark.svg @@ -0,0 +1,259 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vscode-lean4/media/lean-mini-light.svg b/vscode-lean4/media/lean-mini-light.svg new file mode 100644 index 000000000..691cdc68c --- /dev/null +++ b/vscode-lean4/media/lean-mini-light.svg @@ -0,0 +1,259 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vscode-lean4/package.json b/vscode-lean4/package.json index 5f557db4a..e3e29ccd0 100644 --- a/vscode-lean4/package.json +++ b/vscode-lean4/package.json @@ -202,6 +202,12 @@ "title": "Refresh File Dependencies", "description": "Restarts the Lean server for the file that is currently focused to refresh the dependencies." }, + { + "command": "lean4.mathlib.fetchCache", + "category": "Lean 4", + "title": "Mathlib: Fetch Build Artifact Cache", + "description": "Downloads cached Mathlib build artifacts to avoid full elaboration" + }, { "command": "lean4.input.convert", "category": "Lean 4", @@ -261,14 +267,62 @@ { "command": "lean4.docView.showAllAbbreviations", "category": "Lean 4", - "title": "Show all abbreviations", + "title": "Docview: Show All Abbreviations", "description": "Show help page containing all abbreviations and the Unicode characters they map to." }, { "command": "lean4.docView.open", "category": "Lean 4", - "title": "Open Documentation View", + "title": "Docview: Open", "description": "Open documentation found in local 'html' folder in a separate web view panel." + }, + { + "command": "lean4.docView.back", + "category": "Lean 4", + "title": "Docview: Back", + "description": "Go to previous page in documentation view" + }, + { + "command": "lean4.docView.forward", + "category": "Lean 4", + "title": "Docview: Forward", + "description": "Go to next page in documentation view" + }, + { + "command": "lean4.createLibraryProject", + "category": "Lean 4", + "title": "Lake: Create Library Project", + "description": "Create a new Lean library project" + }, + { + "command": "lean4.createProgramProject", + "category": "Lean 4", + "title": "Lake: Create Program Project", + "description": "Create a new Lean program project" + }, + { + "command": "lean4.createMathlibProject", + "category": "Lean 4", + "title": "Lake: Create Math Formalization Project", + "description": "Create a new Lean math formalization project" + }, + { + "command": "lean4.cloneProject", + "category": "Lean 4", + "title": "Git: Download Project", + "description": "Download an existing Lean project using `git clone`" + }, + { + "command": "lean4.buildProject", + "category": "Lean 4", + "title": "Lake: Build Project", + "description": "Build the current project" + }, + { + "command": "lean4.cleanProject", + "category": "Lean 4", + "title": "Lake: Clean Project", + "description": "Clean the current project, removing all build artifacts" } ], "languages": [ @@ -346,54 +400,176 @@ "menus": { "commandPalette": [ { - "command": "lean4.input.convert", - "when": "editorLangId == lean4 && lean4.input.isActive" + "command": "lean4.restartServer", + "when": "editorLangId == lean4" + }, + { + "command": "lean4.stopServer", + "when": "editorLangId == lean4" }, { "command": "lean4.restartFile", "when": "editorLangId == lean4" }, + { + "command": "lean4.refreshFileDependencies", + "when": "editorLangId == lean4" + }, + { + "command": "lean4.mathlib.fetchCache", + "when": "editorLangId == lean4" + }, + { + "command": "lean4.input.convert", + "when": "editorLangId == lean4 && lean4.input.isActive" + }, { "command": "lean4.displayGoal", "when": "editorLangId == lean4" }, { - "command": "lean4.infoView.copyToComment", + "command": "lean4.toggleInfoview", "when": "editorLangId == lean4" }, { - "command": "lean4.infoView.toggleStickyPosition", + "command": "lean4.displayList", "when": "editorLangId == lean4" }, { - "command": "lean4.restartServer", + "command": "lean4.infoView.copyToComment", "when": "editorLangId == lean4" }, { - "command": "lean4.stopServer", + "command": "lean4.infoView.toggleStickyPosition", "when": "editorLangId == lean4" }, { - "command": "lean4.restartFile", + "command": "lean4.infoView.toggleUpdating", "when": "editorLangId == lean4" }, { - "command": "lean4.refreshFileDependencies", + "command": "lean4.infoView.toggleExpectedType", "when": "editorLangId == lean4" }, { "command": "lean4.docView.showAllAbbreviations", "when": "editorLangId == lean4" + }, + { + "command": "lean4.docView.open" + }, + { + "command": "lean4.docView.back" + }, + { + "command": "lean4.docView.forward" + }, + { + "command": "lean4.createLibraryProject" + }, + { + "command": "lean4.createProgramProject" + }, + { + "command": "lean4.createMathlibProject" + }, + { + "command": "lean4.cloneProject" + }, + { + "command": "lean4.buildProject" + }, + { + "command": "lean4.cleanProject" } ], "editor/title": [ { - "command": "lean4.displayGoal", + "submenu": "lean4.titlebar", + "when": "editorLangId == lean4", + "group": "navigation@0" + } + ], + "lean4.titlebar": [ + { + "command": "lean4.restartFile", + "when": "editorLangId == lean4", + "group": "1_server@1" + }, + { + "command": "lean4.restartServer", + "when": "editorLangId == lean4", + "group": "1_server@2" + }, + { + "command": "lean4.toggleInfoview", + "when": "editorLangId == lean4", + "group": "2_infoview@1" + }, + { + "command": "lean4.docView.open", + "when": "editorLangId == lean4", + "group": "3_docview@1" + }, + { + "command": "lean4.docView.showAllAbbreviations", + "when": "editorLangId == lean4", + "group": "3_docview@2" + }, + { + "command": "lean4.buildProject", + "when": "editorLangId == lean4", + "group": "4_project@1" + }, + { + "command": "lean4.cleanProject", + "when": "editorLangId == lean4", + "group": "4_project@2" + }, + { + "command": "lean4.mathlib.fetchCache", + "when": "editorLangId == lean4", + "group": "5_mathlib@1" + }, + { + "command": "lean4.createLibraryProject", + "when": "editorLangId == lean4", + "group": "6_setup@1" + }, + { + "command": "lean4.createProgramProject", + "when": "editorLangId == lean4", + "group": "6_setup@2" + }, + { + "command": "lean4.createMathlibProject", "when": "editorLangId == lean4", - "group": "navigation@2" + "group": "6_setup@3" + }, + { + "command": "lean4.cloneProject", + "when": "editorLangId == lean4", + "group": "6_setup@4" + } + ], + "editor/context": [ + { + "command": "lean4.restartFile", + "when": "editorLangId == lean4", + "group": "z_commands" } ] }, + "submenus": [ + { + "id": "lean4.titlebar", + "label": "Lean 4", + "icon": { + "dark": "./media/lean-mini-dark.svg", + "light": "./media/lean-mini-light.svg" + } + } + ], "semanticTokenScopes": [ { "scopes": { @@ -409,7 +585,12 @@ "editor.tabSize": 2, "editor.wordSeparators": "`~@$%^&*()-=+[{]}⟨⟩⦃⦄⟦⟧⟮⟯‹›\\|;:\",.<>/" } - } + }, + "taskDefinitions": [ + { + "type": "lean4" + } + ] }, "extensionKind": [ "workspace" @@ -418,7 +599,8 @@ "onLanguage:lean", "onLanguage:lean4", "onLanguage:markdown", - "onCommand:lean4.restartServer" + "onCommand:lean4.restartServer", + "onCommand:workbench.action.tasks.runTask" ], "main": "./dist/extension", "scripts": { diff --git a/vscode-lean4/src/docview.ts b/vscode-lean4/src/docview.ts index a288ec34f..5178b6e35 100644 --- a/vscode-lean4/src/docview.ts +++ b/vscode-lean4/src/docview.ts @@ -56,11 +56,12 @@ export class DocViewProvider implements Disposable { constructor(extensionUri: Uri) { this.extensionUri = extensionUri; this.subscriptions.push( - commands.registerCommand('lean4.docView.open', (url: string) => this.open(url)), + commands.registerCommand('lean4.docView.open', () => this.open()), + commands.registerCommand('lean4.docView.openUrl', (url: string) => this.open(url)), commands.registerCommand('lean4.docView.back', () => this.back()), commands.registerCommand('lean4.docView.forward', () => this.forward()), - commands.registerCommand('lean4.openTryIt', (code: string) => this.tryIt(code)), - commands.registerCommand('lean4.openExample', (file: string) => this.example(file)), + commands.registerCommand('lean4.docView.openTryIt', (code: string) => this.tryIt(code)), + commands.registerCommand('lean4.docView.openExample', (file: string) => this.example(file)), commands.registerCommand('lean4.docView.showAllAbbreviations', () => this.showAbbreviations()) ); this.subscriptions.push(workspace.onDidCloseTextDocument(doc => { @@ -224,15 +225,15 @@ export class DocViewProvider implements Disposable { } const books : any = { - 'Theorem Proving in Lean': mkCommandUri('lean4.docView.open', 'https://leanprover.github.io/theorem_proving_in_lean4/introduction.html'), - 'Mathematics in Lean': mkCommandUri('lean4.docView.open', 'https://leanprover-community.github.io/mathematics_in_lean/'), - 'Reference Manual': mkCommandUri('lean4.docView.open', 'https://leanprover.github.io/lean4/doc/'), + 'Theorem Proving in Lean': mkCommandUri('lean4.docView.openUrl', 'https://leanprover.github.io/theorem_proving_in_lean4/introduction.html'), + 'Mathematics in Lean': mkCommandUri('lean4.docView.openUrl', 'https://leanprover-community.github.io/mathematics_in_lean/'), + 'Reference Manual': mkCommandUri('lean4.docView.openUrl', 'https://leanprover.github.io/lean4/doc/'), 'Abbreviations Cheatsheet': mkCommandUri('lean4.docView.showAllAbbreviations'), - 'Example': mkCommandUri('lean4.openExample', 'https://github.com/leanprover/lean4-samples/raw/main/HelloWorld/Main.lean'), + 'Example': mkCommandUri('lean4.docView.openExample', 'https://github.com/leanprover/lean4-samples/raw/main/HelloWorld/Main.lean'), // These are handy for testing that the bad file logic is working. - //'Test bad file': mkCommandUri('lean4.docView.open', Uri.joinPath(this.extensionUri, 'media', 'webview.js')), - //'Test bad Uri': mkCommandUri('lean4.docView.open', 'https://leanprover.github.io/lean4/doc/images/code-success.png'), + //'Test bad file': mkCommandUri('lean4.docView.openUrl', Uri.joinPath(this.extensionUri, 'media', 'webview.js')), + //'Test bad Uri': mkCommandUri('lean4.docView.openUrl', 'https://leanprover.github.io/lean4/doc/images/code-success.png'), }; for (const book of Object.getOwnPropertyNames(books)) { @@ -317,17 +318,17 @@ var side_bar = false; // collapse the side bar menu by default. // here when the html is round tripped through the cheerio parser. } else if (link.attribs.tryitfile) { link.attribs.title = link.attribs.title || 'Open code block (in existing file)'; - link.attribs.href = mkCommandUri('lean4.openExample', new URL(link.attribs.tryitfile as string, url).toString()); + link.attribs.href = mkCommandUri('lean4.docView.openExample', new URL(link.attribs.tryitfile as string, url).toString()); } else if (tryItMatch) { const code = decodeURIComponent(tryItMatch[1] as string); link.attribs.title = link.attribs.title || 'Open code block in new editor'; - link.attribs.href = mkCommandUri('lean4.openTryIt', code); + link.attribs.href = mkCommandUri('lean4.docView.openTryIt', code); } else if (!link.attribs.href.startsWith('command:')) { const hrefUrl = new URL(link.attribs.href as string, url); const isExternal = !url || new URL(url).origin !== hrefUrl.origin; if (!isExternal || hrefUrl.protocol === 'file:') { link.attribs.title = link.attribs.title || link.attribs.href; - link.attribs.href = mkCommandUri('lean4.docView.open', hrefUrl.toString()); + link.attribs.href = mkCommandUri('lean4.docView.openUrl', hrefUrl.toString()); } } } @@ -378,7 +379,7 @@ var side_bar = false; // collapse the side bar menu by default. } /** Called by the user clicking a link. */ - async open(url?: string): Promise { + async open(url?: string | undefined): Promise { if (this.currentURL) { this.backStack.push(this.currentURL); this.forwardStack = []; diff --git a/vscode-lean4/src/exports.ts b/vscode-lean4/src/exports.ts index fe13010ba..8388474a2 100644 --- a/vscode-lean4/src/exports.ts +++ b/vscode-lean4/src/exports.ts @@ -2,12 +2,16 @@ import { InfoProvider } from './infoview' import { DocViewProvider } from './docview'; import { LeanInstaller } from './utils/leanInstaller' import { LeanClientProvider } from './utils/clientProvider'; +import { LeanTaskProvider } from './tasks'; +import { ProjectOperationProvider } from './project'; export interface Exports { - isLean4Project: boolean; - version: string; - infoProvider: InfoProvider | undefined; - clientProvider: LeanClientProvider | undefined; - installer : LeanInstaller | undefined; - docView : DocViewProvider | undefined; + isLean4Project: boolean + version: string + infoProvider: InfoProvider | undefined + clientProvider: LeanClientProvider | undefined + installer: LeanInstaller | undefined + docView: DocViewProvider | undefined + taskProvider: LeanTaskProvider | undefined + projectOperationProvider: ProjectOperationProvider | undefined } diff --git a/vscode-lean4/src/extension.ts b/vscode-lean4/src/extension.ts index fe69d5c10..c1e938e18 100644 --- a/vscode-lean4/src/extension.ts +++ b/vscode-lean4/src/extension.ts @@ -1,4 +1,4 @@ -import { window, Uri, workspace, ExtensionContext, TextDocument } from 'vscode' +import { window, ExtensionContext, TextDocument, tasks } from 'vscode' import { AbbreviationFeature } from './abbreviation' import { InfoProvider } from './infoview' import { DocViewProvider } from './docview'; @@ -9,7 +9,9 @@ import { LeanClientProvider } from './utils/clientProvider'; import { addDefaultElanPath, removeElanPath, addToolchainBinPath, isElanDisabled, getDefaultLeanVersion} from './config'; import { findLeanPackageVersionInfo } from './utils/projectInfo'; import { Exports } from './exports'; +import { LeanTaskProvider, leanTaskDefinition } from './tasks'; import { logger } from './utils/logger' +import { ProjectOperationProvider } from './project'; function isLean(languageId : string) : boolean { return languageId === 'lean' || languageId === 'lean4'; @@ -21,8 +23,7 @@ function getLeanDocument() : TextDocument | undefined { if (window.activeTextEditor && isLean(window.activeTextEditor.document.languageId)) { document = window.activeTextEditor.document - } - else { + } else { // This happens if vscode starts with a lean file open // but the "Getting Started" page is active. for (const editor of window.visibleTextEditors) { @@ -60,7 +61,7 @@ export async function activate(context: ExtensionContext): Promise { if (toolchainVersion && toolchainVersion.indexOf('lean:3') > 0) { logger.log(`Lean4 skipping lean 3 project: ${toolchainVersion}`); return { isLean4Project: false, version: toolchainVersion, - infoProvider: undefined, clientProvider: undefined, installer: undefined, docView: undefined }; + infoProvider: undefined, clientProvider: undefined, installer: undefined, docView: undefined, taskProvider: undefined, projectOperationProvider: undefined }; } } @@ -76,7 +77,7 @@ export async function activate(context: ExtensionContext): Promise { // We need to terminate before registering the LeanClientProvider, // because that class changes the document id to `lean4`. return { isLean4Project: false, version: '3', - infoProvider: undefined, clientProvider: undefined, installer: undefined, docView: undefined }; + infoProvider: undefined, clientProvider: undefined, installer: undefined, docView: undefined, taskProvider: undefined, projectOperationProvider: undefined }; } const pkgService = new LeanpkgService() @@ -102,6 +103,12 @@ export async function activate(context: ExtensionContext): Promise { pkgService.versionChanged((uri) => installer.handleVersionChanged(uri)); pkgService.lakeFileChanged((uri) => installer.handleLakeFileChanged(uri)); + const taskProvider = new LeanTaskProvider() + context.subscriptions.push(tasks.registerTaskProvider(leanTaskDefinition.type, taskProvider)) + + const projectOperationProvider = new ProjectOperationProvider() + context.subscriptions.push(projectOperationProvider) + return { isLean4Project: true, version: '4', - infoProvider: info, clientProvider: leanClientProvider, installer, docView}; + infoProvider: info, clientProvider: leanClientProvider, installer, docView, taskProvider, projectOperationProvider}; } diff --git a/vscode-lean4/src/project.ts b/vscode-lean4/src/project.ts new file mode 100644 index 000000000..b0bad69e6 --- /dev/null +++ b/vscode-lean4/src/project.ts @@ -0,0 +1,159 @@ +import * as vscode from 'vscode'; +import { Disposable, TaskRevealKind, Uri, commands, window, workspace, SaveDialogOptions } from 'vscode'; +import { LeanTask, buildTask, cacheGetTask, cleanTask, createExecutableTask, runTaskUntilCompletion, updateTask } from './tasks'; +import path = require('path'); + + +export class ProjectOperationProvider implements Disposable { + + private subscriptions: Disposable[] = []; + + constructor() { + this.subscriptions.push( + commands.registerCommand('lean4.createLibraryProject', () => this.createLibraryProject()), + commands.registerCommand('lean4.createProgramProject', () => this.createProgramProject()), + commands.registerCommand('lean4.createMathlibProject', () => this.createMathlibProject()), + commands.registerCommand('lean4.cloneProject', () => this.cloneProject()), + commands.registerCommand('lean4.buildProject', () => this.buildProject()), + commands.registerCommand('lean4.cleanProject', () => this.cleanProject()), + commands.registerCommand('lean4.mathlib.fetchCache', () => this.fetchMathlibCache()) + ) + } + + private async createLibraryProject() { + await this.createProject('lib', 'library') + } + + private async createProgramProject() { + await this.createProject('exe', 'program') + } + + private async createMathlibProject() { + await this.createProject('math', 'math formalization', + 'leanprover-community/mathlib4:lean-toolchain', + [updateTask, cacheGetTask]) + } + + private async createProject( + kind: string, + kindName: string, + toolchain?: string | undefined, + postProcessingTasks: LeanTask[] = []) { + + const projectFolder: Uri | undefined = await this.askForNewProjectFolderLocation({ + saveLabel: 'Create project folder', + title: `Create a new ${kindName} project folder` + }) + if (projectFolder === undefined) { + return + } + + await workspace.fs.createDirectory(projectFolder) + + const projectName: string = path.basename(projectFolder.fsPath) + const initCommand: string = + toolchain === undefined + ? 'init' + : `+${toolchain} init` + const createProjectTask: LeanTask = { + command: `lake ${initCommand} "${projectName}" ${kind}`, + description: `Create new Lean 4 ${kindName} project` + } + + const tasks = postProcessingTasks.slice() + tasks.unshift(createProjectTask) + for (const task of tasks) { + try { + await runTaskUntilCompletion(createExecutableTask(task, TaskRevealKind.Always, projectFolder.fsPath), this.subscriptions) + } catch (e) { + return // error will already be displayed in terminal + } + } + + await this.openFolder(projectFolder) + } + + private async cloneProject() { + const unparsedProjectUri: string | undefined = await window.showInputBox({ + title: 'URL Input', + value: 'https://github.com/leanprover-community/mathlib4', + prompt: 'URL of Git repository for existing Lean 4 project', + validateInput: value => { + try { + Uri.parse(value, true) + return undefined // valid URI + } catch (e) { + return 'Invalid URL' + } + } + }) + if (unparsedProjectUri === undefined) { + return + } + const existingProjectUri = Uri.parse(unparsedProjectUri) + + const projectFolder: Uri | undefined = await this.askForNewProjectFolderLocation({ + saveLabel: 'Create project folder', + title: 'Create a new project folder to clone existing project into' + }) + if (projectFolder === undefined) { + return + } + + try { + await runTaskUntilCompletion(createExecutableTask({ + command: `git clone "${existingProjectUri}" "${projectFolder.fsPath}"`, + description: 'Download existing Lean 4 project using `git clone`' + }), this.subscriptions) + } catch (e) { + return // error will already be displayed in terminal + } + + await this.openFolder(projectFolder) + } + + private async askForNewProjectFolderLocation(options: SaveDialogOptions): Promise { + const projectFolder: Uri | undefined = await window.showSaveDialog(options) + if (projectFolder === undefined) { + return undefined + } + if (projectFolder.scheme !== 'file') { + await window.showErrorMessage('Project folder must be created in a file system.') + return undefined + } + return projectFolder + } + + private async openFolder(projectFolder: Uri) { + const message: string = ` + Project initialized. Open new project folder '${path.basename(projectFolder.fsPath)}'? + Unsaved file contents will be lost. + ` + const choice: string | undefined = await window.showWarningMessage(message, { modal: true }, 'Open project folder') + if (choice === 'Open project folder') { + // this kills the extension host, so it has to be the last command + await commands.executeCommand('vscode.openFolder', projectFolder) + } + } + + private async buildProject() { + await vscode.tasks.executeTask(createExecutableTask(buildTask)) + } + + private async cleanProject() { + const choice: string | undefined = await window.showWarningMessage('Delete all build artifacts?', { modal: true }, 'Proceed') + + if (choice === 'Proceed') { + await vscode.tasks.executeTask(createExecutableTask(cleanTask)) + } + } + + private async fetchMathlibCache() { + await vscode.tasks.executeTask(createExecutableTask(cacheGetTask)) + } + + dispose() { + for (const s of this.subscriptions) { s.dispose(); } + } + +} diff --git a/vscode-lean4/src/tasks.ts b/vscode-lean4/src/tasks.ts new file mode 100644 index 000000000..962fc7931 --- /dev/null +++ b/vscode-lean4/src/tasks.ts @@ -0,0 +1,87 @@ +import { CancellationToken, ProviderResult, ShellExecution, Task, TaskProvider, TaskScope, TaskDefinition, TaskRevealKind, tasks, Disposable } from 'vscode' + +export const leanTaskDefinition: TaskDefinition = { type: 'lean4' } + +export interface LeanTask { + command: string + description: string +} + +export function createExecutableTask(task: LeanTask, reveal: TaskRevealKind = TaskRevealKind.Always, cwd?: string | undefined): Task { + const t = new Task( + leanTaskDefinition, + TaskScope.Workspace, + task.description, + 'Lean 4', + new ShellExecution(task.command, { cwd }), + '' + ) + t.presentationOptions.reveal = reveal + return t +} + +export async function runTaskUntilCompletion(task: Task, subscriptions: Disposable[]): Promise { + const execution = await tasks.executeTask(task) + return new Promise((resolve, reject) => { + tasks.onDidEndTaskProcess(async e => { + if (e.execution !== execution) { + return + } + + if (e.exitCode === 0) { + resolve() + } else { + reject(e.exitCode) + } + }, undefined, subscriptions) + }) +} + +export const initLibraryProjectTask: LeanTask = { + command: 'lake init ${workspaceFolderBasename} lib', + description: 'Initialize Lean 4 library project in current folder' +} +export const initProgramProjectTask: LeanTask = { + command: 'lake init ${workspaceFolderBasename} exe', + description: 'Initialize Lean 4 program project in current folder' +} +export const initMathlibProjectTask: LeanTask = { + command: 'lake +leanprover-community/mathlib4:lean-toolchain init ${workspaceFolderBasename} math', + description: 'Initialize Lean 4 math formalization project in current folder' +} +export const buildTask: LeanTask = { + command: 'lake build', + description: 'Build Lean 4 project' +} +export const cleanTask: LeanTask = { + command: 'lake clean', + description: 'Clean build artifacts of Lean 4 project' +} +export const cacheGetTask: LeanTask = { + command: 'lake exe cache get', + description: '⚠ Mathlib command ⚠: Download cached Mathlib build artifacts' +} +export const updateTask: LeanTask = { + command: 'lake update', + description: '⚠ Project maintenance command ⚠: Upgrade all project dependencies' +} + +export class LeanTaskProvider implements TaskProvider { + + provideTasks(token: CancellationToken): ProviderResult { + return [ + createExecutableTask(initLibraryProjectTask, TaskRevealKind.Silent), + createExecutableTask(initProgramProjectTask, TaskRevealKind.Silent), + createExecutableTask(initMathlibProjectTask, TaskRevealKind.Silent), + createExecutableTask(buildTask), + createExecutableTask(cleanTask), + createExecutableTask(cacheGetTask), + createExecutableTask(updateTask) + ] + } + + resolveTask(task: Task, token: CancellationToken): ProviderResult { + return undefined + } + +} diff --git a/vscode-lean4/src/utils/clientProvider.ts b/vscode-lean4/src/utils/clientProvider.ts index b3272aa55..42b666e7d 100644 --- a/vscode-lean4/src/utils/clientProvider.ts +++ b/vscode-lean4/src/utils/clientProvider.ts @@ -145,8 +145,14 @@ export class LeanClientProvider implements Disposable { void this.activeClient?.stop(); } - private restartActiveClient() { - void this.activeClient?.restart(); + private async restartActiveClient() { + const result: string | undefined = await window.showWarningMessage( + 'Restart Lean 4 server to re-elaborate all open files?', + { modal: true }, + 'Restart server') + if (result === 'Restart server') { + void this.activeClient?.restart(); + } } clientIsStarted() { From f3c650beccc53b75dffcb0d10dd8a589094dc4ae Mon Sep 17 00:00:00 2001 From: mhuisi Date: Fri, 15 Sep 2023 14:05:38 +0200 Subject: [PATCH 02/18] fix?: use correct env for tasks --- vscode-lean4/src/tasks.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/vscode-lean4/src/tasks.ts b/vscode-lean4/src/tasks.ts index 962fc7931..a5978148c 100644 --- a/vscode-lean4/src/tasks.ts +++ b/vscode-lean4/src/tasks.ts @@ -8,12 +8,19 @@ export interface LeanTask { } export function createExecutableTask(task: LeanTask, reveal: TaskRevealKind = TaskRevealKind.Always, cwd?: string | undefined): Task { + const env = Object.entries(process.env) + .filter(([_, value]) => value !== undefined) + .reduce((obj, [key, value]) => { + obj[key] = value as string; + return obj; + }, {} as {[key: string]: string}); + const t = new Task( leanTaskDefinition, TaskScope.Workspace, task.description, 'Lean 4', - new ShellExecution(task.command, { cwd }), + new ShellExecution(task.command, { cwd, env }), '' ) t.presentationOptions.reveal = reveal From 7f061759469fcf9cfad0d7fb34887b50f9626a21 Mon Sep 17 00:00:00 2001 From: mhuisi Date: Fri, 15 Sep 2023 16:10:31 +0200 Subject: [PATCH 03/18] bit of cleanup --- vscode-lean4/src/project.ts | 4 ++-- vscode-lean4/src/tasks.ts | 2 ++ vscode-lean4/src/utils/clientProvider.ts | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/vscode-lean4/src/project.ts b/vscode-lean4/src/project.ts index b0bad69e6..d2cf95cbf 100644 --- a/vscode-lean4/src/project.ts +++ b/vscode-lean4/src/project.ts @@ -129,7 +129,7 @@ export class ProjectOperationProvider implements Disposable { Project initialized. Open new project folder '${path.basename(projectFolder.fsPath)}'? Unsaved file contents will be lost. ` - const choice: string | undefined = await window.showWarningMessage(message, { modal: true }, 'Open project folder') + const choice: string | undefined = await window.showInformationMessage(message, { modal: true }, 'Open project folder') if (choice === 'Open project folder') { // this kills the extension host, so it has to be the last command await commands.executeCommand('vscode.openFolder', projectFolder) @@ -141,7 +141,7 @@ export class ProjectOperationProvider implements Disposable { } private async cleanProject() { - const choice: string | undefined = await window.showWarningMessage('Delete all build artifacts?', { modal: true }, 'Proceed') + const choice: string | undefined = await window.showInformationMessage('Delete all build artifacts?', { modal: true }, 'Proceed') if (choice === 'Proceed') { await vscode.tasks.executeTask(createExecutableTask(cleanTask)) diff --git a/vscode-lean4/src/tasks.ts b/vscode-lean4/src/tasks.ts index a5978148c..8e80fb44a 100644 --- a/vscode-lean4/src/tasks.ts +++ b/vscode-lean4/src/tasks.ts @@ -8,6 +8,8 @@ export interface LeanTask { } export function createExecutableTask(task: LeanTask, reveal: TaskRevealKind = TaskRevealKind.Always, cwd?: string | undefined): Task { + // use `process.env` because if users just installed elan, the default parent process env + // of the task will not contain the elan executables, while `process.env` does const env = Object.entries(process.env) .filter(([_, value]) => value !== undefined) .reduce((obj, [key, value]) => { diff --git a/vscode-lean4/src/utils/clientProvider.ts b/vscode-lean4/src/utils/clientProvider.ts index 42b666e7d..1d45c2505 100644 --- a/vscode-lean4/src/utils/clientProvider.ts +++ b/vscode-lean4/src/utils/clientProvider.ts @@ -146,7 +146,7 @@ export class LeanClientProvider implements Disposable { } private async restartActiveClient() { - const result: string | undefined = await window.showWarningMessage( + const result: string | undefined = await window.showInformationMessage( 'Restart Lean 4 server to re-elaborate all open files?', { modal: true }, 'Restart server') From d36476e53fcba1bcb61e82c6d0d23892071a5b82 Mon Sep 17 00:00:00 2001 From: mhuisi Date: Fri, 22 Sep 2023 18:56:45 +0200 Subject: [PATCH 04/18] command reorganization, guide, safer open, new activation logic --- .vscode/settings.json | 5 +- vscode-lean4/media/guide-documentation.md | 32 ++ vscode-lean4/media/guide-help.md | 6 + vscode-lean4/media/guide-installDeps-linux.md | 18 + vscode-lean4/media/guide-installDeps-mac.md | 16 + .../media/guide-installDeps-windows.md | 9 + vscode-lean4/media/guide-installElan-unix.md | 4 + .../media/guide-installElan-windows.md | 4 + vscode-lean4/media/guide-setupProject.md | 17 + vscode-lean4/media/open-setup-guide.png | Bin 0 -> 17901 bytes vscode-lean4/package.json | 365 ++++++++++++++---- vscode-lean4/src/config.ts | 4 + vscode-lean4/src/exports.ts | 2 +- vscode-lean4/src/extension.ts | 201 +++++++--- vscode-lean4/src/leanclient.ts | 2 +- vscode-lean4/src/project.ts | 126 ++++-- vscode-lean4/src/tasks.ts | 16 +- vscode-lean4/src/utils/clientProvider.ts | 48 ++- vscode-lean4/src/utils/leanInstaller.ts | 95 +++-- vscode-lean4/src/utils/projectInfo.ts | 33 +- 20 files changed, 774 insertions(+), 229 deletions(-) create mode 100644 vscode-lean4/media/guide-documentation.md create mode 100644 vscode-lean4/media/guide-help.md create mode 100644 vscode-lean4/media/guide-installDeps-linux.md create mode 100644 vscode-lean4/media/guide-installDeps-mac.md create mode 100644 vscode-lean4/media/guide-installDeps-windows.md create mode 100644 vscode-lean4/media/guide-installElan-unix.md create mode 100644 vscode-lean4/media/guide-installElan-windows.md create mode 100644 vscode-lean4/media/guide-setupProject.md create mode 100644 vscode-lean4/media/open-setup-guide.png diff --git a/.vscode/settings.json b/.vscode/settings.json index 512315c94..5e457541a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,5 +8,8 @@ }, "typescript.tsdk": "./node_modules/typescript/lib", // we want to use the TS server from our node_modules folder to control its version "files.insertFinalNewline": true, - "files.trimTrailingWhitespace": true + "files.trimTrailingWhitespace": true, + "[markdown]": { + "files.trimTrailingWhitespace": false + } } diff --git a/vscode-lean4/media/guide-documentation.md b/vscode-lean4/media/guide-documentation.md new file mode 100644 index 000000000..0be45e07e --- /dev/null +++ b/vscode-lean4/media/guide-documentation.md @@ -0,0 +1,32 @@ +## Books +If you want to learn Lean 4, choose one of the following introductory books based on your background. If you are getting stuck or have any questions, click on the 'Questions and Troubleshooting' step below. + +- [Theorem Proving in Lean 4](https://lean-lang.org/theorem_proving_in_lean4/) + The standard reference for using Lean 4 as an interactive theorem prover. Suited as an introduction for users with a computer science background, advanced users and for general use as a reference manual. +- [Mathematics in Lean](https://leanprover-community.github.io/mathematics_in_lean/) + The standard introduction to Lean 4 as an interactive theorem prover for users with a mathematics background. +- [Functional Programming in Lean](https://lean-lang.org/functional_programming_in_lean/) + The standard introduction for using Lean 4 as a general-purpose programming language. + +Once you have completed one of these books and its exercises, you are ready to use Lean 4 for your own projects. If you want to use Lean 4 both as an interactive theorem prover and as a general-purpose programming language, it is recommended to read both [Functional Programming in Lean](https://lean-lang.org/functional_programming_in_lean/) and [Theorem Proving in Lean 4](https://lean-lang.org/theorem_proving_in_lean4/). + +## Hands-On Tutorial +If you want to dive right into using Lean 4 to prove elementary theorems about natural numbers, you can play the [Natural Number Game](https://adam.math.hhu.de/#/g/hhu-adam/NNG4). It can be played online using your browser without a local installation. + +## Documentation +**Metaprogramming** +[Metaprogramming in Lean 4](https://github.com/leanprover-community/lean4-metaprogramming-book) is an introduction to the metaprogramming facilities of Lean 4 that can be used to write theorem proving automation, adjust Lean 4 and extend Lean 4. Should only be read after [Functional Programming in Lean](https://lean-lang.org/functional_programming_in_lean/). + +**Type System** +If you are a type theorist and want to learn the details of the type theory underlying Lean 4, you can read [The Type Theory of Lean](https://github.com/digama0/lean-type-theory/releases/download/v1.0/main.pdf) by Mario Carneiro for a formal description of the type theory of Lean 3, which is largely identical to that of Lean 4. The few aspects that have changed between Lean 3 and Lean 4 are described in section 3.2 of [An Extensible Theorem Proving +Frontend](https://lean-lang.org/papers/thesis-sebastian.pdf) by Sebastian Ullrich. + +**Architecture** +Many elements of the architecture of Lean 4, for example its macro system, its do-notation, its reference counting garbage collection and the different components which Lean 4 is composed of are described in [An Extensible Theorem Proving +Frontend](https://lean-lang.org/papers/thesis-sebastian.pdf) by Sebastian Ullrich. + +**Manual** +The [Lean Manual](https://lean-lang.org/lean4/doc/) is a loose collection of details on Lean 4. + +## Additional Resources +The [Lean Community website](https://leanprover-community.github.io/index.html) links to several other learning resources not listed here that may be helpful. diff --git a/vscode-lean4/media/guide-help.md b/vscode-lean4/media/guide-help.md new file mode 100644 index 000000000..80aecdd83 --- /dev/null +++ b/vscode-lean4/media/guide-help.md @@ -0,0 +1,6 @@ +## Asking Questions on the Lean Zulip Chat + +To post your question on the [Lean Zulip chat](https://leanprover.zulipchat.com/), you can follow these steps: +1. [Create a new Lean Zulip chat account](https://leanprover.zulipchat.com/register/). +2. [Visit the #new-members stream](https://leanprover.zulipchat.com/#narrow/stream/113489-new-members). +3. Click the 'New topic' button at the bottom of the page, enter a topic title, describe your question or issue in the message text box and click 'Send'. diff --git a/vscode-lean4/media/guide-installDeps-linux.md b/vscode-lean4/media/guide-installDeps-linux.md new file mode 100644 index 000000000..8ece155d0 --- /dev/null +++ b/vscode-lean4/media/guide-installDeps-linux.md @@ -0,0 +1,18 @@ +## Installing Required Dependencies +1. [Open a new terminal](command:workbench.action.terminal.new). +2. Depending on your Linux distribution, do one of the following to install Git and curl using your package manager: + * On Ubuntu and Debian, type in `sudo apt install git curl` and press Enter. + * On Fedora, type in `sudo dnf install git curl` and press Enter. + * If you are not sure which Linux distribution you are using, you can try both. +3. When prompted, type in your login credentials. +4. Wait until the installation has completed. + +## Dependencies Needed by Lean 4 +[Git](https://git-scm.com/) is a commonly used [Version Control System](https://en.wikipedia.org/wiki/Version_control) that is used by Lean to help manage different versions of Lean formalization packages and software packages. + +[curl](https://curl.se/) is a small tool to transfer data that is used by Lean to download files when managing Lean formalization packages and software packages. + +## Restricted Environments +If you are in an environment where you cannot install Git or curl, for example a restricted university computer, you can check if you already have them installed by [opening a new terminal](command:workbench.action.terminal.new), typing in `which git curl` and pressing Enter. If the terminal output displays two file paths and no error, you already have them installed. + +If your machine does not already have Git and curl installed and you cannot install them, there is currently no option to try Lean 4 with a local installation. If you want to try out Lean 4 regardless, you can read [Mathematics in Lean](https://leanprover-community.github.io/mathematics_in_lean/) and do the exercises with [an online instance of Lean 4 hosted using Gitpod](https://gitpod.io/#/https://github.com/leanprover-community/mathematics_in_lean). Doing so requires creating a GitHub account. diff --git a/vscode-lean4/media/guide-installDeps-mac.md b/vscode-lean4/media/guide-installDeps-mac.md new file mode 100644 index 000000000..9617e2a24 --- /dev/null +++ b/vscode-lean4/media/guide-installDeps-mac.md @@ -0,0 +1,16 @@ +## Installing Required Dependencies +1. [Open a new terminal](command:workbench.action.terminal.new). +2. Type in `/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"` and press Enter to install [Homebrew](https://brew.sh/), a package manager for macOS. +3. Follow the instructions in the terminal. +4. Type in `brew install curl git` and press Enter. +5. Wait until the installation has completed. + +## Dependencies Needed by Lean 4 +[Git](https://git-scm.com/) is a commonly used [Version Control System](https://en.wikipedia.org/wiki/Version_control) that is used by Lean to help manage different versions of Lean formalization packages and software packages. + +[curl](https://curl.se/) is a small tool to transfer data that is used by Lean to download files when managing Lean formalization packages and software packages. + +## Restricted Environments +If you are in an environment where you cannot install Homebrew, Git or curl, for example a restricted university computer, you can check if you already have them installed by [opening a new terminal](command:workbench.action.terminal.new), typing in `which git curl` and pressing Enter. If the terminal output displays two file paths and no error, you already have them installed. + +If your machine does not already have Homebrew, Git and curl installed and you cannot install them, there is currently no option to try Lean 4 with a local installation. If you want to try out Lean 4 regardless, you can read [Mathematics in Lean](https://leanprover-community.github.io/mathematics_in_lean/) and do the exercises with [an online instance of Lean 4 hosted using Gitpod](https://gitpod.io/#/https://github.com/leanprover-community/mathematics_in_lean). Doing so requires creating a GitHub account. diff --git a/vscode-lean4/media/guide-installDeps-windows.md b/vscode-lean4/media/guide-installDeps-windows.md new file mode 100644 index 000000000..324bead4f --- /dev/null +++ b/vscode-lean4/media/guide-installDeps-windows.md @@ -0,0 +1,9 @@ +## Installing Required Dependencies +1. Install [Git](https://git-scm.com/download/win). You can keep all settings in the installer at their default values. +2. Wait until the installation has completed. +3. Restart VS Code and re-open this guide. + +[Git](https://git-scm.com/) is a commonly used [Version Control System](https://en.wikipedia.org/wiki/Version_control) that is used by Lean to help manage different versions of Lean formalization packages and software packages. + +## Restricted Environments +If you are in an environment where you cannot install Git and it is not already installed, for example on a restricted university computer, there is currently no option to try Lean 4 with a local installation. If you want to try out Lean 4 regardless, you can read [Mathematics in Lean](https://leanprover-community.github.io/mathematics_in_lean/) and do the exercises with [an online instance of Lean 4 hosted using Gitpod](https://gitpod.io/#/https://github.com/leanprover-community/mathematics_in_lean). Doing so requires creating a GitHub account. diff --git a/vscode-lean4/media/guide-installElan-unix.md b/vscode-lean4/media/guide-installElan-unix.md new file mode 100644 index 000000000..c17a6a1c9 --- /dev/null +++ b/vscode-lean4/media/guide-installElan-unix.md @@ -0,0 +1,4 @@ +## Elan +[Elan](https://github.com/leanprover/elan) automatically manages all the different versions of Lean and ensures that the correct version is used when opening a project. + +Clicking the 'Click to install' button will download the [Elan setup script](https://github.com/leanprover/elan/blob/master/elan-init.sh) and execute it. diff --git a/vscode-lean4/media/guide-installElan-windows.md b/vscode-lean4/media/guide-installElan-windows.md new file mode 100644 index 000000000..65fbacf42 --- /dev/null +++ b/vscode-lean4/media/guide-installElan-windows.md @@ -0,0 +1,4 @@ +## Elan +[Elan](https://github.com/leanprover/elan) automatically manages all the different versions of Lean and ensures that the correct version is used when opening a project. + +Clicking the 'Click to install' button will download the [Elan setup script](https://github.com/leanprover/elan/blob/master/elan-init.ps1) and execute it. diff --git a/vscode-lean4/media/guide-setupProject.md b/vscode-lean4/media/guide-setupProject.md new file mode 100644 index 000000000..6ce0ef0e3 --- /dev/null +++ b/vscode-lean4/media/guide-setupProject.md @@ -0,0 +1,17 @@ +## Project Creation +If you want to create a new project, click on one of the following: +- [Create a new library project](command:lean4.project.createLibraryProject) + Library projects can be used by other Lean 4 projects. +- [Create a new math formalization project](command:lean4.project.createMathlibProject) + Math formalization projects are library projects that depend on [mathlib](https://github.com/leanprover-community/mathlib4), the math library of Lean 4. +- [Create a new program project](command:lean4.project.createProgramProject) + Program projects allow compiling Lean code to executable programs. + +If you want to open an existing project, click on one of the following: +- [Download an existing project](command:lean4.project.clone) +- [Open an existing local project](command:lean4.project.open) + +After creating or downloading a project, you can open it in the future by clicking the ∀-symbol in the top right, choosing 'Open Project…' > 'Open Local Project…' and selecting the project you created. + +## Complex Project Setups +Using its build system and package manager Lake, Lean 4 supports project setups that are more complex than the ones above. You can find out more about Lake in the [Lean 4 GitHub repository](https://github.com/leanprover/lean4/blob/master/src/lake/README.md). diff --git a/vscode-lean4/media/open-setup-guide.png b/vscode-lean4/media/open-setup-guide.png new file mode 100644 index 0000000000000000000000000000000000000000..c86e984642be2d425451e6f6543bc1fc6eab1456 GIT binary patch literal 17901 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 literal 0 HcmV?d00001 diff --git a/vscode-lean4/package.json b/vscode-lean4/package.json index e3e29ccd0..3317a82cf 100644 --- a/vscode-lean4/package.json +++ b/vscode-lean4/package.json @@ -5,7 +5,7 @@ "version": "0.0.110", "publisher": "leanprover", "engines": { - "vscode": "^1.70.0" + "vscode": "^1.75.0" }, "categories": [ "Programming Languages" @@ -25,7 +25,7 @@ "lean4.toolchainPath": { "type": "string", "default": "", - "markdownDescription": "Path to your Lean toolchain. Leave this blank to get the default location from your PATH environment or from the default elan install location.", + "markdownDescription": "**DO NOT CHANGE** unless you know what you are doing. Path to your Lean toolchain. Leave this blank to get the default location from your PATH environment or from the default elan install location.", "scope": "machine-overridable" }, "lean4.input.enabled": { @@ -174,6 +174,16 @@ "type": "number", "default": 200, "description": "Time (in milliseconds) which must pass since latest edit until elaboration begins. Lower values may make editing feel faster at the cost of higher CPU usage." + }, + "lean4.showInvalidProjectWarnings": { + "type": "boolean", + "default": true, + "markdownDescription": "Show warnings whenever a .lean-file is opened in a folder that does not contain a 'lean-toolchain' file." + }, + "lean4.alwaysShowTitleBarMenu": { + "type": "boolean", + "default": true, + "markdownDescription": "Always display the Lean extension title bar menu in the top right. This helps beginners create and open Lean projects after launching an empty instance of VS Code, but may not be desirable for anyone who uses VS Code for things other than Lean." } } }, @@ -181,33 +191,27 @@ { "command": "lean4.restartServer", "category": "Lean 4", - "title": "Restart Server", + "title": "Server: Restart Server", "description": "Restart the Lean server (for all files)." }, { "command": "lean4.stopServer", "category": "Lean 4", - "title": "Stop Server", + "title": "Server: Stop Server", "description": "Stop the Lean server (for all files)." }, { "command": "lean4.restartFile", "category": "Lean 4", - "title": "Restart File", + "title": "Server: Restart File", "description": "Restarts the Lean server for the file that is currently focused, refreshing the dependencies." }, { "command": "lean4.refreshFileDependencies", "category": "Lean 4", - "title": "Refresh File Dependencies", + "title": "Server: Refresh File Dependencies", "description": "Restarts the Lean server for the file that is currently focused to refresh the dependencies." }, - { - "command": "lean4.mathlib.fetchCache", - "category": "Lean 4", - "title": "Mathlib: Fetch Build Artifact Cache", - "description": "Downloads cached Mathlib build artifacts to avoid full elaboration" - }, { "command": "lean4.input.convert", "category": "Lean 4", @@ -227,7 +231,7 @@ { "command": "lean4.toggleInfoview", "category": "Lean 4", - "title": "Infoview: Toggle", + "title": "Infoview: Toggle Infoview", "description": "Toggle whether the infoview is displayed." }, { @@ -273,7 +277,7 @@ { "command": "lean4.docView.open", "category": "Lean 4", - "title": "Docview: Open", + "title": "Docview: Open Docview", "description": "Open documentation found in local 'html' folder in a separate web view panel." }, { @@ -289,40 +293,64 @@ "description": "Go to next page in documentation view" }, { - "command": "lean4.createLibraryProject", + "command": "lean4.setup.showSetupGuide", + "category": "Lean 4", + "title": "Setup: Show Setup Guide", + "description": "Show 'Welcome' page containing a checklist of steps to install Lean 4." + }, + { + "command": "lean4.setup.installElan", "category": "Lean 4", - "title": "Lake: Create Library Project", + "title": "Setup: Install Elan", + "description": "Install Lean's version manager 'Elan'" + }, + { + "command": "lean4.project.createLibraryProject", + "category": "Lean 4", + "title": "Project: Create Library Project…", "description": "Create a new Lean library project" }, { - "command": "lean4.createProgramProject", + "command": "lean4.project.createProgramProject", "category": "Lean 4", - "title": "Lake: Create Program Project", + "title": "Project: Create Program Project…", "description": "Create a new Lean program project" }, { - "command": "lean4.createMathlibProject", + "command": "lean4.project.createMathlibProject", "category": "Lean 4", - "title": "Lake: Create Math Formalization Project", + "title": "Project: Create Math Formalization Project…", "description": "Create a new Lean math formalization project" }, { - "command": "lean4.cloneProject", + "command": "lean4.project.open", "category": "Lean 4", - "title": "Git: Download Project", + "title": "Project: Open Local Project…", + "description": "Opens a local Lean project" + }, + { + "command": "lean4.project.clone", + "category": "Lean 4", + "title": "Project: Download Project…", "description": "Download an existing Lean project using `git clone`" }, { - "command": "lean4.buildProject", + "command": "lean4.project.build", "category": "Lean 4", - "title": "Lake: Build Project", + "title": "Project: Build Project", "description": "Build the current project" }, { - "command": "lean4.cleanProject", + "command": "lean4.project.clean", "category": "Lean 4", - "title": "Lake: Clean Project", + "title": "Project: Clean Project", "description": "Clean the current project, removing all build artifacts" + }, + { + "command": "lean4.project.fetchCache", + "category": "Lean 4", + "title": "Project: Fetch Mathlib Build Cache", + "description": "Downloads cached Mathlib build artifacts to avoid full elaboration" } ], "languages": [ @@ -415,10 +443,6 @@ "command": "lean4.refreshFileDependencies", "when": "editorLangId == lean4" }, - { - "command": "lean4.mathlib.fetchCache", - "when": "editorLangId == lean4" - }, { "command": "lean4.input.convert", "when": "editorLangId == lean4 && lean4.input.isActive" @@ -465,91 +489,142 @@ "command": "lean4.docView.forward" }, { - "command": "lean4.createLibraryProject" + "command": "lean4.setup.showSetupGuide" }, { - "command": "lean4.createProgramProject" + "command": "lean4.setup.installElan" }, { - "command": "lean4.createMathlibProject" + "command": "lean4.project.createLibraryProject" }, { - "command": "lean4.cloneProject" + "command": "lean4.project.createProgramProject" }, { - "command": "lean4.buildProject" + "command": "lean4.project.createMathlibProject" }, { - "command": "lean4.cleanProject" + "command": "lean4.project.open" + }, + { + "command": "lean4.project.clone" + }, + { + "command": "lean4.project.build" + }, + { + "command": "lean4.project.clean" + }, + { + "command": "lean4.project.fetchCache", + "when": "editorLangId == lean4" } ], "editor/title": [ { "submenu": "lean4.titlebar", - "when": "editorLangId == lean4", + "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", "group": "navigation@0" } ], "lean4.titlebar": [ + { + "submenu": "lean4.titlebar.newProject", + "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", + "group": "1_setup@1" + }, + { + "submenu": "lean4.titlebar.openProject", + "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", + "group": "1_setup@1" + }, { "command": "lean4.restartFile", "when": "editorLangId == lean4", - "group": "1_server@1" + "group": "2_server@1" }, { "command": "lean4.restartServer", "when": "editorLangId == lean4", - "group": "1_server@2" + "group": "2_server@2" }, { "command": "lean4.toggleInfoview", "when": "editorLangId == lean4", - "group": "2_infoview@1" + "group": "3_infoview@1" }, { - "command": "lean4.docView.open", + "submenu": "lean4.titlebar.projectActions", "when": "editorLangId == lean4", - "group": "3_docview@1" + "group": "4_projectActions@1" }, { - "command": "lean4.docView.showAllAbbreviations", - "when": "editorLangId == lean4", - "group": "3_docview@2" + "submenu": "lean4.titlebar.documentation", + "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", + "group": "5_documentation@1" + } + ], + "lean4.titlebar.newProject": [ + { + "command": "lean4.project.createLibraryProject", + "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", + "group": "1_newProject@1" }, { - "command": "lean4.buildProject", - "when": "editorLangId == lean4", - "group": "4_project@1" + "command": "lean4.project.createProgramProject", + "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", + "group": "1_newProject@2" }, { - "command": "lean4.cleanProject", - "when": "editorLangId == lean4", - "group": "4_project@2" + "command": "lean4.project.createMathlibProject", + "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", + "group": "1_newProject@2" + } + ], + "lean4.titlebar.openProject": [ + { + "command": "lean4.project.open", + "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", + "group": "1_openProject@1" }, { - "command": "lean4.mathlib.fetchCache", + "command": "lean4.project.clone", + "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", + "group": "1_openProject@2" + } + ], + "lean4.titlebar.projectActions": [ + { + "command": "lean4.project.build", "when": "editorLangId == lean4", - "group": "5_mathlib@1" + "group": "4_project@1" }, { - "command": "lean4.createLibraryProject", + "command": "lean4.project.clean", "when": "editorLangId == lean4", - "group": "6_setup@1" + "group": "4_project@2" }, { - "command": "lean4.createProgramProject", + "command": "lean4.project.fetchCache", "when": "editorLangId == lean4", - "group": "6_setup@2" + "group": "5_mathlib@1" + } + ], + "lean4.titlebar.documentation": [ + { + "command": "lean4.setup.showSetupGuide", + "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", + "group": "1_installation@1" }, { - "command": "lean4.createMathlibProject", - "when": "editorLangId == lean4", - "group": "6_setup@3" + "command": "lean4.docView.open", + "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", + "group": "2_docview@1" }, { - "command": "lean4.cloneProject", - "when": "editorLangId == lean4", - "group": "6_setup@4" + "command": "lean4.docView.showAllAbbreviations", + "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", + "group": "2_docview@2" } ], "editor/context": [ @@ -568,6 +643,22 @@ "dark": "./media/lean-mini-dark.svg", "light": "./media/lean-mini-light.svg" } + }, + { + "id": "lean4.titlebar.newProject", + "label": "New Project…" + }, + { + "id": "lean4.titlebar.openProject", + "label": "Open Project…" + }, + { + "id": "lean4.titlebar.projectActions", + "label": "Project Actions…" + }, + { + "id": "lean4.titlebar.documentation", + "label": "Show Documentation…" } ], "semanticTokenScopes": [ @@ -590,6 +681,149 @@ { "type": "lean4" } + ], + "walkthroughs": [ + { + "id": "guide.linux", + "title": "Lean 4 Setup", + "description": "Getting started with Lean 4 on Linux\n", + "when": "isLinux", + "steps": [ + { + "id": "guide.linux.openSetupGuide", + "title": "Re-Open Setup Guide", + "description": "This guide can always be re-opened by clicking on the ∀-symbol in the top right and selecting 'Show Documentation…' > 'Setup: Show Setup Guide'.", + "media": { + "image": "media/open-setup-guide.png", + "altText": "Click on the ∀-symbol in the top right and select 'Show Documentation…' > 'Setup: Show Setup Guide'." + } + }, + { + "id": "guide.linux.documentation", + "title": "Books and Documentation", + "description": "Learn using Lean 4 with the resources on the right.", + "media": { "markdown": "./media/guide-documentation.md" } + }, + { + "id": "guide.linux.installDeps", + "title": "Install Required Dependencies", + "description": "Install Git and curl using your package manager.", + "media": { "markdown": "./media/guide-installDeps-linux.md" } + }, + { + "id": "guide.linux.installElan", + "title": "Install Lean Version Manager", + "description": "Install Lean's version manager Elan.\n[Click to install](command:lean4.setup.installElan)", + "media": { "markdown": "./media/guide-installElan-unix.md" } + }, + { + "id": "guide.linux.setupProject", + "title": "Set Up Lean 4 Project", + "description": "Set up a Lean 4 project by clicking on one of the options on the right.", + "media": { "markdown": "./media/guide-setupProject.md" } + }, + { + "id": "guide.linux.help", + "title": "Questions and Troubleshooting", + "description": "If you have any questions or are having trouble with any of the previous steps, please visit us on the [Lean Zulip chat](https://leanprover.zulipchat.com/) so that we can help you.", + "media": { "markdown": "./media/guide-help.md" } + } + ] + }, + { + "id": "guide.mac", + "title": "Lean 4 Setup", + "description": "Getting started with Lean 4 on Mac\n", + "when": "isMac", + "steps": [ + { + "id": "guide.mac.openSetupGuide", + "title": "Re-Open Setup Guide", + "description": "This guide can always be re-opened by clicking on the ∀-symbol in the top right and selecting 'Show Documentation…' > 'Setup: Show Setup Guide'.", + "media": { + "image": "media/open-setup-guide.png", + "altText": "Click on the ∀-symbol in the top right and select 'Show Documentation…' > 'Setup: Show Setup Guide'." + } + }, + { + "id": "guide.mac.documentation", + "title": "Books and Documentation", + "description": "Learn using Lean 4 with the resources on the right.", + "media": { "markdown": "./media/guide-documentation.md" } + }, + { + "id": "guide.mac.installDeps", + "title": "Install Required Dependencies", + "description": "Install Homebrew, Git and curl.", + "media": { "markdown": "./media/guide-installDeps-mac.md" } + }, + { + "id": "guide.mac.installElan", + "title": "Install Lean Version Manager", + "description": "Install Lean's version manager Elan.\n[Click to install](command:lean4.setup.installElan)", + "media": { "markdown": "./media/guide-installElan-unix.md" } + }, + { + "id": "guide.mac.setupProject", + "title": "Set Up Lean 4 Project", + "description": "Set up a Lean 4 project by clicking on one of the options on the right.", + "media": { "markdown": "./media/guide-setupProject.md" } + }, + { + "id": "guide.mac.help", + "title": "Questions and Troubleshooting", + "description": "If you have any questions or are having trouble with any of the previous steps, please visit us on the [Lean Zulip chat](https://leanprover.zulipchat.com/) so that we can help you.", + "media": { "markdown": "./media/guide-help.md" } + } + ] + }, + { + "id": "guide.windows", + "title": "Lean 4 Setup", + "description": "Getting started with Lean 4 on Windows\n", + "when": "isWindows", + "steps": [ + { + "id": "guide.windows.openSetupGuide", + "title": "Re-Open Setup Guide", + "description": "This guide can always be re-opened by clicking on the ∀-symbol in the top right and selecting 'Show Documentation…' > 'Setup: Show Setup Guide'.", + "media": { + "image": "media/open-setup-guide.png", + "altText": "Click on the ∀-symbol in the top right and select 'Show Documentation…' > 'Setup: Show Setup Guide'." + } + }, + { + "id": "guide.windows.documentation", + "title": "Books and Documentation", + "description": "Learn using Lean 4 with the resources on the right.", + "media": { "markdown": "./media/guide-documentation.md" } + }, + { + "id": "guide.windows.installDeps", + "title": "Install Required Dependencies", + "description": "Install Git.", + "media": { "markdown": "./media/guide-installDeps-windows.md" } + }, + { + "id": "guide.windows.installElan", + "title": "Install Lean Version Manager", + "description": "Install Lean's version manager Elan.\n[Click to install](command:lean4.setup.installElan)", + "media": { "markdown": "./media/guide-installElan-windows.md" } + }, + { + "id": "guide.windows.setupProject", + "title": "Set Up Lean 4 Project", + "description": "Set up a Lean 4 project by clicking on one of the options on the right.", + "media": { "markdown": "./media/guide-setupProject.md" } + }, + { + "id": "guide.windows.help", + "title": "Questions and Troubleshooting", + "description": "If you have any questions or are having trouble with any of the previous steps, please visit us on the [Lean Zulip chat](https://leanprover.zulipchat.com/) so that we can help you.", + "media": { "markdown": "./media/guide-help.md" } + } + ] + } ] }, "extensionKind": [ @@ -597,10 +831,9 @@ ], "activationEvents": [ "onLanguage:lean", - "onLanguage:lean4", "onLanguage:markdown", - "onCommand:lean4.restartServer", - "onCommand:workbench.action.tasks.runTask" + "onCommand:workbench.action.tasks.runTask", + "onStartupFinished" ], "main": "./dist/extension", "scripts": { diff --git a/vscode-lean4/src/config.ts b/vscode-lean4/src/config.ts index b29a1c98c..d990fd3c2 100644 --- a/vscode-lean4/src/config.ts +++ b/vscode-lean4/src/config.ts @@ -219,6 +219,10 @@ export function getElaborationDelay(): number { return workspace.getConfiguration('lean4').get('elaborationDelay', 200); } +export function shouldShowInvalidProjectWarnings(): boolean { + return workspace.getConfiguration('lean4').get('showInvalidProjectWarnings', true) +} + export function getLeanExecutableName(): string { if (process.platform === 'win32') { return 'lean.exe' diff --git a/vscode-lean4/src/exports.ts b/vscode-lean4/src/exports.ts index 8388474a2..e5e98c382 100644 --- a/vscode-lean4/src/exports.ts +++ b/vscode-lean4/src/exports.ts @@ -7,7 +7,7 @@ import { ProjectOperationProvider } from './project'; export interface Exports { isLean4Project: boolean - version: string + version: string | undefined infoProvider: InfoProvider | undefined clientProvider: LeanClientProvider | undefined installer: LeanInstaller | undefined diff --git a/vscode-lean4/src/extension.ts b/vscode-lean4/src/extension.ts index c1e938e18..8c32949f9 100644 --- a/vscode-lean4/src/extension.ts +++ b/vscode-lean4/src/extension.ts @@ -1,4 +1,4 @@ -import { window, ExtensionContext, TextDocument, tasks } from 'vscode' +import { window, ExtensionContext, TextDocument, tasks, commands } from 'vscode' import { AbbreviationFeature } from './abbreviation' import { InfoProvider } from './infoview' import { DocViewProvider } from './docview'; @@ -13,33 +13,43 @@ import { LeanTaskProvider, leanTaskDefinition } from './tasks'; import { logger } from './utils/logger' import { ProjectOperationProvider } from './project'; +interface AlwaysEnabledFeatures { + docView: DocViewProvider + taskProvider: LeanTaskProvider + projectOperationProvider: ProjectOperationProvider + installer: LeanInstaller +} + +interface Lean4EnabledFeatures { + clientProvider: LeanClientProvider + infoProvider: InfoProvider +} + function isLean(languageId : string) : boolean { return languageId === 'lean' || languageId === 'lean4'; } +function findOpenLeanDocument() : TextDocument | undefined { + if (window.activeTextEditor && isLean(window.activeTextEditor.document.languageId)) { + return window.activeTextEditor.document + } -function getLeanDocument() : TextDocument | undefined { - let document : TextDocument | undefined; - if (window.activeTextEditor && isLean(window.activeTextEditor.document.languageId)) - { - document = window.activeTextEditor.document - } else { - // This happens if vscode starts with a lean file open - // but the "Getting Started" page is active. - for (const editor of window.visibleTextEditors) { - const lang = editor.document.languageId; - if (isLean(lang)) { - document = editor.document; - break; - } + // This happens if vscode starts with a lean file open + // but the "Getting Started" page is active. + for (const editor of window.visibleTextEditors) { + if (isLean(editor.document.languageId)) { + return editor.document } } - return document; -} -export async function activate(context: ExtensionContext): Promise { + return undefined; +} - // for unit test that tests behavior when there is no elan installed. +/** + * Activates all extension features that are *always* enabled, even when no Lean 4 document is currently open. + */ +function activateAlwaysEnabledFeatures(context: ExtensionContext): AlwaysEnabledFeatures { + // For unit test that tests behavior when there is no elan installed. if (isElanDisabled()) { const elanRoot = removeElanPath(); if (elanRoot){ @@ -49,66 +59,131 @@ export async function activate(context: ExtensionContext): Promise { addDefaultElanPath(); } - const defaultToolchain = getDefaultLeanVersion(); - - // note: workspace.rootPath can be undefined in the untitled or adhoc case - // where the user ran "code lean_filename". - const doc = getLeanDocument(); - let packageUri = null; - let toolchainVersion = null; - if (doc) { - [packageUri, toolchainVersion] = await findLeanPackageVersionInfo(doc.uri); - if (toolchainVersion && toolchainVersion.indexOf('lean:3') > 0) { - logger.log(`Lean4 skipping lean 3 project: ${toolchainVersion}`); - return { isLean4Project: false, version: toolchainVersion, - infoProvider: undefined, clientProvider: undefined, installer: undefined, docView: undefined, taskProvider: undefined, projectOperationProvider: undefined }; + context.subscriptions.push(commands.registerCommand('lean4.setup.showSetupGuide', async () => { + if (process.platform === 'win32') { + await commands.executeCommand('workbench.action.openWalkthrough', 'leanprover.lean4#guide.windows', false) + } else if (process.platform === 'darwin') { + await commands.executeCommand('workbench.action.openWalkthrough', 'leanprover.lean4#guide.mac', false) + } else if (process.platform === 'linux') { + await commands.executeCommand('workbench.action.openWalkthrough', 'leanprover.lean4#guide.linux', false) + } else { + await commands.executeCommand('workbench.action.openWalkthrough', 'leanprover.lean4#guide.linux', false) } - } + })) - const outputChannel = window.createOutputChannel('Lean: Editor'); + const docView = new DocViewProvider(context.extensionUri); + context.subscriptions.push(docView); + // safe + const taskProvider = new LeanTaskProvider() + context.subscriptions.push(tasks.registerTaskProvider(leanTaskDefinition.type, taskProvider)) + + // safe + const projectOperationProvider = new ProjectOperationProvider() + context.subscriptions.push(projectOperationProvider) + + const outputChannel = window.createOutputChannel('Lean: Editor'); + const defaultToolchain = getDefaultLeanVersion(); const installer = new LeanInstaller(outputChannel, defaultToolchain) - context.subscriptions.push(installer); - const versionInfo = await installer.checkLeanVersion(packageUri, toolchainVersion??defaultToolchain) - // Check whether rootPath is a Lean 3 project (the Lean 3 extension also uses the deprecated rootPath) - if (versionInfo.version === '3') { - context.subscriptions.pop()?.dispose(); // stop installer - // We need to terminate before registering the LeanClientProvider, - // because that class changes the document id to `lean4`. - return { isLean4Project: false, version: '3', - infoProvider: undefined, clientProvider: undefined, installer: undefined, docView: undefined, taskProvider: undefined, projectOperationProvider: undefined }; - } + return { docView, taskProvider, projectOperationProvider, installer } +} - const pkgService = new LeanpkgService() - context.subscriptions.push(pkgService); +async function isLean3Project(installer: LeanInstaller): Promise { + const doc = findOpenLeanDocument(); + + const [packageUri, toolchainVersion] = doc + ? await findLeanPackageVersionInfo(doc.uri) + : [null, null] - const leanClientProvider = new LeanClientProvider(installer, pkgService, outputChannel); - context.subscriptions.push(leanClientProvider) + if (toolchainVersion && toolchainVersion.indexOf('lean:3') > 0) { + logger.log(`Lean4 skipping lean 3 project: ${toolchainVersion}`); + return true + } + + const versionInfo = await installer.checkLeanVersion(packageUri, toolchainVersion ?? installer.getDefaultToolchain()) + if (versionInfo.version === '3') { + return true + } - const info = new InfoProvider(leanClientProvider, {language: 'lean4'}, context); - context.subscriptions.push(info) + return false +} +function activateAbbreviationFeature(context: ExtensionContext, docView: DocViewProvider): AbbreviationFeature { const abbrev = new AbbreviationFeature(); + // Pass the abbreviations through to the docView so it can show them on demand. + docView.setAbbreviations(abbrev.abbreviations.symbolsByAbbreviation); context.subscriptions.push(abbrev); + return abbrev +} - const docView = new DocViewProvider(context.extensionUri); - context.subscriptions.push(docView); +function activateLean4Features(context: ExtensionContext, installer: LeanInstaller): Lean4EnabledFeatures { + // unsafe + const pkgService = new LeanpkgService() + pkgService.versionChanged((uri) => installer.handleVersionChanged(uri)); + pkgService.lakeFileChanged((uri) => installer.handleLakeFileChanged(uri)); + context.subscriptions.push(pkgService); - // pass the abbreviations through to the docView so it can show them on demand. - docView.setAbbreviations(abbrev.abbreviations.symbolsByAbbreviation); + // unsafe + const clientProvider = new LeanClientProvider(installer, pkgService, installer.getOutputChannel()); + context.subscriptions.push(clientProvider) - context.subscriptions.push(new LeanTaskGutter(leanClientProvider, context)) + // unsafe + const infoProvider = new InfoProvider(clientProvider, {language: 'lean4'}, context); + context.subscriptions.push(infoProvider) - pkgService.versionChanged((uri) => installer.handleVersionChanged(uri)); - pkgService.lakeFileChanged((uri) => installer.handleLakeFileChanged(uri)); + context.subscriptions.push(new LeanTaskGutter(clientProvider, context)) - const taskProvider = new LeanTaskProvider() - context.subscriptions.push(tasks.registerTaskProvider(leanTaskDefinition.type, taskProvider)) + return { clientProvider, infoProvider } +} - const projectOperationProvider = new ProjectOperationProvider() - context.subscriptions.push(projectOperationProvider) +export async function activate(context: ExtensionContext): Promise { + const alwaysEnabledFeatures: AlwaysEnabledFeatures = activateAlwaysEnabledFeatures(context) + + if (await isLean3Project(alwaysEnabledFeatures.installer)) { + return { + isLean4Project: false, + version: '3', + infoProvider: undefined, + clientProvider: undefined, + installer: alwaysEnabledFeatures.installer, + docView: alwaysEnabledFeatures.docView, + taskProvider: alwaysEnabledFeatures.taskProvider, + projectOperationProvider: alwaysEnabledFeatures.projectOperationProvider + } + } - return { isLean4Project: true, version: '4', - infoProvider: info, clientProvider: leanClientProvider, installer, docView, taskProvider, projectOperationProvider}; + activateAbbreviationFeature(context, alwaysEnabledFeatures.docView) + + if (findOpenLeanDocument()) { + const lean4EnabledFeatures: Lean4EnabledFeatures = activateLean4Features(context, alwaysEnabledFeatures.installer) + return { + isLean4Project: true, + version: '4', + infoProvider: lean4EnabledFeatures.infoProvider, + clientProvider: lean4EnabledFeatures.clientProvider, + installer: alwaysEnabledFeatures.installer, + docView: alwaysEnabledFeatures.docView, + taskProvider: alwaysEnabledFeatures.taskProvider, + projectOperationProvider: alwaysEnabledFeatures.projectOperationProvider + } + } + + // No Lean 4 document yet => Load remaining features when one is open + window.onDidChangeVisibleTextEditors(_ => { + if (findOpenLeanDocument()) { + activateLean4Features(context, alwaysEnabledFeatures.installer) + } + }) + + return { + isLean4Project: false, + version: undefined, + infoProvider: undefined, + clientProvider: undefined, + installer: alwaysEnabledFeatures.installer, + docView: alwaysEnabledFeatures.docView, + taskProvider: alwaysEnabledFeatures.taskProvider, + projectOperationProvider: alwaysEnabledFeatures.projectOperationProvider + } } diff --git a/vscode-lean4/src/leanclient.ts b/vscode-lean4/src/leanclient.ts index 97a6335aa..1e1aa8dab 100644 --- a/vscode-lean4/src/leanclient.ts +++ b/vscode-lean4/src/leanclient.ts @@ -45,7 +45,7 @@ export class LeanClient implements Disposable { private toolchainPath: string private outputChannel: OutputChannel; private workspaceFolder: WorkspaceFolder | undefined; - private folderUri: Uri; + folderUri: Uri; private subscriptions: Disposable[] = [] private noPrompt : boolean = false; private showingRestartMessage : boolean = false; diff --git a/vscode-lean4/src/project.ts b/vscode-lean4/src/project.ts index d2cf95cbf..3a7a3c4a1 100644 --- a/vscode-lean4/src/project.ts +++ b/vscode-lean4/src/project.ts @@ -1,8 +1,8 @@ import * as vscode from 'vscode'; -import { Disposable, TaskRevealKind, Uri, commands, window, workspace, SaveDialogOptions } from 'vscode'; -import { LeanTask, buildTask, cacheGetTask, cleanTask, createExecutableTask, runTaskUntilCompletion, updateTask } from './tasks'; +import { Disposable, TaskRevealKind, Uri, commands, window, workspace, SaveDialogOptions, FileType } from 'vscode'; +import { LeanTask, buildTask, cacheGetTask, cleanTask, createExecutableTask, runTaskUntilCompletion, updateElanTask, updateTask } from './tasks'; import path = require('path'); - +import { checkParentFoldersForLeanProject, isValidLeanProject } from './utils/projectInfo'; export class ProjectOperationProvider implements Disposable { @@ -10,22 +10,23 @@ export class ProjectOperationProvider implements Disposable { constructor() { this.subscriptions.push( - commands.registerCommand('lean4.createLibraryProject', () => this.createLibraryProject()), - commands.registerCommand('lean4.createProgramProject', () => this.createProgramProject()), - commands.registerCommand('lean4.createMathlibProject', () => this.createMathlibProject()), - commands.registerCommand('lean4.cloneProject', () => this.cloneProject()), - commands.registerCommand('lean4.buildProject', () => this.buildProject()), - commands.registerCommand('lean4.cleanProject', () => this.cleanProject()), - commands.registerCommand('lean4.mathlib.fetchCache', () => this.fetchMathlibCache()) + commands.registerCommand('lean4.project.createLibraryProject', () => this.createLibraryProject()), + commands.registerCommand('lean4.project.createProgramProject', () => this.createProgramProject()), + commands.registerCommand('lean4.project.createMathlibProject', () => this.createMathlibProject()), + commands.registerCommand('lean4.project.open', () => this.openProject()), + commands.registerCommand('lean4.project.clone', () => this.cloneProject()), + commands.registerCommand('lean4.project.build', () => this.buildProject()), + commands.registerCommand('lean4.project.clean', () => this.cleanProject()), + commands.registerCommand('lean4.project.fetchCache', () => this.fetchMathlibCache()) ) } private async createLibraryProject() { - await this.createProject('lib', 'library') + await this.createProject('lib', 'library', 'stable') } private async createProgramProject() { - await this.createProject('exe', 'program') + await this.createProject('exe', 'program', 'stable') } private async createMathlibProject() { @@ -40,7 +41,7 @@ export class ProjectOperationProvider implements Disposable { toolchain?: string | undefined, postProcessingTasks: LeanTask[] = []) { - const projectFolder: Uri | undefined = await this.askForNewProjectFolderLocation({ + const projectFolder: Uri | undefined = await ProjectOperationProvider.askForNewProjectFolderLocation({ saveLabel: 'Create project folder', title: `Create a new ${kindName} project folder` }) @@ -60,17 +61,64 @@ export class ProjectOperationProvider implements Disposable { description: `Create new Lean 4 ${kindName} project` } - const tasks = postProcessingTasks.slice() - tasks.unshift(createProjectTask) + const tasks = [updateElanTask, createProjectTask].concat(postProcessingTasks) for (const task of tasks) { try { await runTaskUntilCompletion(createExecutableTask(task, TaskRevealKind.Always, projectFolder.fsPath), this.subscriptions) } catch (e) { - return // error will already be displayed in terminal + return // Error will already be displayed in terminal + } + } + + await ProjectOperationProvider.openNewFolder(projectFolder) + } + + private async openProject() { + const projectFolders: Uri[] | undefined = await window.showOpenDialog({ + title: 'Open Lean 4 project folder containing a `lean-toolchain` file', + openLabel: 'Open project folder', + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false + }) + if (projectFolders === undefined || projectFolders.length !== 1) { + return + } + + let projectFolder = projectFolders[0] + if (!await ProjectOperationProvider.checkIsFileUriOrShowError(projectFolder)) { + return + } + + if (!await isValidLeanProject(projectFolder)) { + const parentProjectFolder: Uri | undefined = await ProjectOperationProvider.attemptFindingLeanProjectInParentFolder(projectFolder) + if (parentProjectFolder === undefined) { + return } + projectFolder = parentProjectFolder } - await this.openFolder(projectFolder) + // This kills the extension host, so it has to be the last command + await commands.executeCommand('vscode.openFolder', projectFolder) + } + + private static async attemptFindingLeanProjectInParentFolder(projectFolder: Uri): Promise { + const parentProjectFolder: Uri | undefined = await checkParentFoldersForLeanProject(projectFolder) + if (parentProjectFolder === undefined) { + await window.showErrorMessage('The selected folder is not a valid Lean 4 project folder. Please make sure to select a folder containing a \'lean-toolchain\' file.') + return undefined + } + + const message = `The selected folder is not a valid Lean 4 project folder because it does not contain a 'lean-toolchain' file. +However, a valid Lean 4 project folder was found in one of the parent directories at ${parentProjectFolder.fsPath}. +Open this project instead?` + const input = 'Open parent directory project' + const choice: string | undefined = await window.showInformationMessage(message, { modal: true }, input) + if (choice !== input) { + return undefined + } + + return parentProjectFolder } private async cloneProject() { @@ -81,7 +129,7 @@ export class ProjectOperationProvider implements Disposable { validateInput: value => { try { Uri.parse(value, true) - return undefined // valid URI + return undefined // Valid URI } catch (e) { return 'Invalid URL' } @@ -92,9 +140,9 @@ export class ProjectOperationProvider implements Disposable { } const existingProjectUri = Uri.parse(unparsedProjectUri) - const projectFolder: Uri | undefined = await this.askForNewProjectFolderLocation({ + const projectFolder: Uri | undefined = await ProjectOperationProvider.askForNewProjectFolderLocation({ saveLabel: 'Create project folder', - title: 'Create a new project folder to clone existing project into' + title: 'Create a new project folder to clone existing Lean 4 project into' }) if (projectFolder === undefined) { return @@ -106,32 +154,35 @@ export class ProjectOperationProvider implements Disposable { description: 'Download existing Lean 4 project using `git clone`' }), this.subscriptions) } catch (e) { - return // error will already be displayed in terminal + return // Error will already be displayed in terminal } - await this.openFolder(projectFolder) + await ProjectOperationProvider.openNewFolder(projectFolder) } - private async askForNewProjectFolderLocation(options: SaveDialogOptions): Promise { + private static async askForNewProjectFolderLocation(options: SaveDialogOptions): Promise { const projectFolder: Uri | undefined = await window.showSaveDialog(options) - if (projectFolder === undefined) { + if (projectFolder === undefined || !await this.checkIsFileUriOrShowError(projectFolder)) { return undefined } - if (projectFolder.scheme !== 'file') { + return projectFolder + } + + private static async checkIsFileUriOrShowError(projectFolder: Uri): Promise { + if (projectFolder.scheme === 'file') { + return true + } else { await window.showErrorMessage('Project folder must be created in a file system.') - return undefined + return false } - return projectFolder } - private async openFolder(projectFolder: Uri) { - const message: string = ` - Project initialized. Open new project folder '${path.basename(projectFolder.fsPath)}'? - Unsaved file contents will be lost. - ` - const choice: string | undefined = await window.showInformationMessage(message, { modal: true }, 'Open project folder') - if (choice === 'Open project folder') { - // this kills the extension host, so it has to be the last command + private static async openNewFolder(projectFolder: Uri) { + const message = `Project initialized. Open new project folder '${path.basename(projectFolder.fsPath)}'?` + const input = 'Open project folder' + const choice: string | undefined = await window.showInformationMessage(message, { modal: true }, input) + if (choice === input) { + // This kills the extension host, so it has to be the last command await commands.executeCommand('vscode.openFolder', projectFolder) } } @@ -141,9 +192,10 @@ export class ProjectOperationProvider implements Disposable { } private async cleanProject() { - const choice: string | undefined = await window.showInformationMessage('Delete all build artifacts?', { modal: true }, 'Proceed') + const input = 'Proceed' + const choice: string | undefined = await window.showInformationMessage('Delete all build artifacts?', { modal: true }, input) - if (choice === 'Proceed') { + if (choice === input) { await vscode.tasks.executeTask(createExecutableTask(cleanTask)) } } diff --git a/vscode-lean4/src/tasks.ts b/vscode-lean4/src/tasks.ts index 8e80fb44a..bca5706a7 100644 --- a/vscode-lean4/src/tasks.ts +++ b/vscode-lean4/src/tasks.ts @@ -8,7 +8,7 @@ export interface LeanTask { } export function createExecutableTask(task: LeanTask, reveal: TaskRevealKind = TaskRevealKind.Always, cwd?: string | undefined): Task { - // use `process.env` because if users just installed elan, the default parent process env + // Use `process.env` because if users just installed elan, the default parent process env // of the task will not contain the elan executables, while `process.env` does const env = Object.entries(process.env) .filter(([_, value]) => value !== undefined) @@ -46,12 +46,16 @@ export async function runTaskUntilCompletion(task: Task, subscriptions: Disposab }) } +export const updateElanTask: LeanTask = { + command: 'elan self update', + description: 'Update Lean\'s version manager Elan' +} export const initLibraryProjectTask: LeanTask = { - command: 'lake init ${workspaceFolderBasename} lib', + command: 'lake +stable init ${workspaceFolderBasename} lib', description: 'Initialize Lean 4 library project in current folder' } export const initProgramProjectTask: LeanTask = { - command: 'lake init ${workspaceFolderBasename} exe', + command: 'lake +stable init ${workspaceFolderBasename} exe', description: 'Initialize Lean 4 program project in current folder' } export const initMathlibProjectTask: LeanTask = { @@ -70,6 +74,10 @@ export const cacheGetTask: LeanTask = { command: 'lake exe cache get', description: '⚠ Mathlib command ⚠: Download cached Mathlib build artifacts' } +export const cachePackTask: LeanTask = { + command: 'lake exe cache pack', + description: '⚠ Mathlib command ⚠: Compress and cache local build artifacts' +} export const updateTask: LeanTask = { command: 'lake update', description: '⚠ Project maintenance command ⚠: Upgrade all project dependencies' @@ -79,12 +87,14 @@ export class LeanTaskProvider implements TaskProvider { provideTasks(token: CancellationToken): ProviderResult { return [ + createExecutableTask(updateElanTask), createExecutableTask(initLibraryProjectTask, TaskRevealKind.Silent), createExecutableTask(initProgramProjectTask, TaskRevealKind.Silent), createExecutableTask(initMathlibProjectTask, TaskRevealKind.Silent), createExecutableTask(buildTask), createExecutableTask(cleanTask), createExecutableTask(cacheGetTask), + createExecutableTask(cachePackTask), createExecutableTask(updateTask) ] } diff --git a/vscode-lean4/src/utils/clientProvider.ts b/vscode-lean4/src/utils/clientProvider.ts index 1d45c2505..fe1763cba 100644 --- a/vscode-lean4/src/utils/clientProvider.ts +++ b/vscode-lean4/src/utils/clientProvider.ts @@ -4,10 +4,10 @@ import { LeanpkgService } from './leanpkg'; import { LeanClient } from '../leanclient' import { LeanFileProgressProcessingInfo, ServerStoppedReason } from '@leanprover/infoview-api'; import * as path from 'path'; -import { findLeanPackageRoot } from './projectInfo'; +import { checkParentFoldersForLeanProject, findLeanPackageRoot, isValidLeanProject } from './projectInfo'; import { isFileInFolder } from './fsHelper'; import { logger } from './logger' -import { addDefaultElanPath, getDefaultElanPath, addToolchainBinPath, isElanDisabled, isRunningTest } from '../config' +import { addDefaultElanPath, getDefaultElanPath, addToolchainBinPath, isElanDisabled, isRunningTest, shouldShowInvalidProjectWarnings } from '../config' // This class ensures we have one LeanClient per workspace folder. export class LeanClientProvider implements Disposable { @@ -51,7 +51,8 @@ export class LeanClientProvider implements Disposable { commands.registerCommand('lean4.restartFile', () => this.restartFile()), commands.registerCommand('lean4.refreshFileDependencies', () => this.restartFile()), commands.registerCommand('lean4.restartServer', () => this.restartActiveClient()), - commands.registerCommand('lean4.stopServer', () => this.stopActiveClient()) + commands.registerCommand('lean4.stopServer', () => this.stopActiveClient()), + commands.registerCommand('lean4.setup.installElan', () => this.autoInstall()) ); workspace.onDidOpenTextDocument((document) => this.didOpenEditor(document)); @@ -115,8 +116,6 @@ export class LeanClientProvider implements Disposable { private async autoInstall() : Promise { // no prompt, just do it! - const version = this.installer.getDefaultToolchain(); - logger.log(`[ClientProvider] Installing ${version} via Elan during testing`); await this.installer.installElan(); if (isElanDisabled()) { addToolchainBinPath(getDefaultElanPath()); @@ -192,9 +191,13 @@ export class LeanClientProvider implements Disposable { try { const [cached, client] = await this.ensureClient(document.uri, undefined); - if (client) { - await client.openLean4Document(document) + if (!client) { + return } + + await this.checkIsValidProjectFolder(client.folderUri) + + await client.openLean4Document(document) } catch (e) { logger.log(`[ClientProvider] ### Error opening document: ${e}`); } @@ -362,6 +365,37 @@ export class LeanClientProvider implements Disposable { return [cachedClient, client]; } + private async checkIsValidProjectFolder(folderUri: Uri) { + if (!shouldShowInvalidProjectWarnings()) { + return + } + + if (folderUri.scheme !== 'file') { + void window.showWarningMessage('Lean 4 server operating in restricted single file mode. Please open a valid Lean 4 project containing a \'lean-toolchain\' file for full functionality.') + return + } + + if (await isValidLeanProject(folderUri)) { + return + } + + const parentProjectFolder: Uri | undefined = await checkParentFoldersForLeanProject(folderUri) + if (parentProjectFolder === undefined) { + void window.showWarningMessage('Opened folder is not a valid Lean 4 project. Please open a valid Lean 4 project containing a \'lean-toolchain\' file for full functionality.') + return + } + + const message = `Opened folder is not a valid Lean 4 project folder because it does not contain a 'lean-toolchain' file. +However, a valid Lean 4 project folder was found in one of the parent directories at ${parentProjectFolder.fsPath}. +Open this project instead?` + const input = 'Open parent directory project' + const choice: string | undefined = await window.showWarningMessage(message, input) + if (choice === input) { + // this kills the extension host + await commands.executeCommand('vscode.openFolder', parentProjectFolder) + } + } + dispose(): void { for (const s of this.subscriptions) { s.dispose(); } } diff --git a/vscode-lean4/src/utils/leanInstaller.ts b/vscode-lean4/src/utils/leanInstaller.ts index e851832d8..2ef383d6f 100644 --- a/vscode-lean4/src/utils/leanInstaller.ts +++ b/vscode-lean4/src/utils/leanInstaller.ts @@ -10,12 +10,11 @@ export class LeanVersion { error: string | undefined; } -export class LeanInstaller implements Disposable { +export class LeanInstaller { private leanInstallerLinux = 'https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh' private leanInstallerWindows = 'https://raw.githubusercontent.com/leanprover/elan/master/elan-init.ps1' private outputChannel: OutputChannel; - private subscriptions: Disposable[] = []; private prompting : boolean = false; private defaultToolchain : string; // the default to use if there is no elan installed private elanDefaultToolchain : string = ''; // the default toolchain according to elan (toolchain marked with '(default)') @@ -44,6 +43,10 @@ export class LeanInstaller implements Disposable { return this.promptUser; } + getOutputChannel(): OutputChannel { + return this.outputChannel + } + async testLeanVersion(packageUri: Uri) : Promise { // see if there is a lean-toolchain file and use that version info. @@ -159,7 +162,7 @@ export class LeanInstaller implements Disposable { const item = await this.showPrompt(prompt, installItem) if (item === installItem) { try { - const result = await this.installElan(); + await this.installElan(); this.installChangedEmitter.fire(uri); } catch (err) { const msg = '' + err; @@ -206,7 +209,7 @@ export class LeanInstaller implements Disposable { } } - const env = addServerEnvPaths(process.env); + addServerEnvPaths(process.env); let options = ['--version'] if (version) { @@ -354,60 +357,54 @@ export class LeanInstaller implements Disposable { } async installElan() : Promise { - if (toolchainPath()) { void window.showErrorMessage('It looks like you\'ve modified the `lean.toolchainPath` user setting.' + 'Please clear this setting before installing elan.'); return false; - } else { - const terminalName = 'Lean installation via elan'; + } - let terminalOptions: TerminalOptions = { name: terminalName }; - if (process.platform === 'win32') { - terminalOptions = { name: terminalName, shellPath: getPowerShellPath() }; - } - const terminal = window.createTerminal(terminalOptions); - terminal.show(); - - // We register a listener, to restart the Lean extension once elan has finished. - const result = new Promise(function(resolve, reject) { - window.onDidCloseTerminal(async (t) => { - if (t === terminal) { - resolve(true); - } else { - logger.log('[LeanInstaller] ignoring terminal closed: ' + t.name + ', waiting for: ' + terminalName); - }}); - }); + const terminalName = 'Lean installation via elan'; - if (process.platform === 'win32') { - terminal.sendText( - `Start-BitsTransfer -Source "${this.leanInstallerWindows}" -Destination "elan-init.ps1"\r\n` + - 'Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope Process\r\n' + - `$rc = .\\elan-init.ps1 -NoPrompt 1 -DefaultToolchain ${this.defaultToolchain}\r\n` + - 'Write-Host "elan-init returned [$rc]"\r\n' + - 'del .\\elan-init.ps1\r\n' + - 'if ($rc -ne 0) {\r\n' + - ' Read-Host -Prompt "Press ENTER to continue"\r\n' + - '}\r\n' + - 'exit\r\n' - ); - } - else { - const elanArgs = `-y --default-toolchain ${this.defaultToolchain}`; - const prompt = '(echo && read -n 1 -s -r -p "Install failed, press ENTER to continue...")'; - - terminal.sendText(`bash -c 'curl ${this.leanInstallerLinux} -sSf | sh -s -- ${elanArgs} || ${prompt}' && exit `); - } + let terminalOptions: TerminalOptions = { name: terminalName }; + if (process.platform === 'win32') { + terminalOptions = { name: terminalName, shellPath: getPowerShellPath() }; + } + const terminal = window.createTerminal(terminalOptions); + terminal.show(); + + // We register a listener, to restart the Lean extension once elan has finished. + const result = new Promise(function(resolve, reject) { + window.onDidCloseTerminal(async (t) => { + if (t === terminal) { + resolve(true); + } else { + logger.log('[LeanInstaller] ignoring terminal closed: ' + t.name + ', waiting for: ' + terminalName); + }}); + }); - // clear any previous lean version errors. - this.versionCache.clear(); - this.elanDefaultToolchain = this.defaultToolchain; + if (process.platform === 'win32') { + terminal.sendText( + `Start-BitsTransfer -Source "${this.leanInstallerWindows}" -Destination "elan-init.ps1"\r\n` + + 'Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope Process\r\n' + + `$rc = .\\elan-init.ps1 -NoPrompt 1 -DefaultToolchain ${this.defaultToolchain}\r\n` + + 'Write-Host "elan-init returned [$rc]"\r\n' + + 'del .\\elan-init.ps1\r\n' + + 'if ($rc -ne 0) {\r\n' + + ' Read-Host -Prompt "Press ENTER to continue"\r\n' + + '}\r\n' + + 'exit\r\n' + ); + } else { + const elanArgs = `-y --default-toolchain ${this.defaultToolchain}`; + const prompt = '(echo && read -n 1 -s -r -p "Install failed, press ENTER to continue...")'; - return result; + terminal.sendText(`bash -c 'curl ${this.leanInstallerLinux} -sSf | sh -s -- ${elanArgs} || ${prompt}' && exit `); } - } - dispose(): void { - for (const s of this.subscriptions) { s.dispose(); } + // clear any previous lean version errors. + this.versionCache.clear(); + this.elanDefaultToolchain = this.defaultToolchain; + + return result; } } diff --git a/vscode-lean4/src/utils/projectInfo.ts b/vscode-lean4/src/utils/projectInfo.ts index c8102ef7d..236c0c36d 100644 --- a/vscode-lean4/src/utils/projectInfo.ts +++ b/vscode-lean4/src/utils/projectInfo.ts @@ -1,8 +1,9 @@ import * as fs from 'fs'; import { URL } from 'url'; -import { Uri, workspace, WorkspaceFolder } from 'vscode'; +import { FileType, Uri, workspace, WorkspaceFolder } from 'vscode'; import { fileExists } from './fsHelper'; import { logger } from './logger' +import path = require('path'); // Detect lean4 root directory (works for both lean4 repo and nightly distribution) @@ -144,3 +145,33 @@ async function readLeanVersionFile(packageFileUri : Uri) : Promise { return ''; } + +export async function isValidLeanProject(projectFolder: Uri): Promise { + try { + const leanToolchainPath = Uri.joinPath(projectFolder, 'lean-toolchain').fsPath + const licensePath = Uri.joinPath(projectFolder, 'LICENSE').fsPath + const licensesPath = Uri.joinPath(projectFolder, 'LICENSES').fsPath + const srcPath = Uri.joinPath(projectFolder, 'src').fsPath + + const isLeanProject: boolean = await fileExists(leanToolchainPath) + const isLeanItself: boolean = + await fileExists(licensePath) && + await fileExists(licensesPath) && + await fileExists(srcPath) + return isLeanProject || isLeanItself + } catch { + return false + } +} + +export async function checkParentFoldersForLeanProject(folder: Uri): Promise { + let childFolder: Uri + do { + childFolder = folder + folder = Uri.file(path.dirname(folder.fsPath)) + if (await isValidLeanProject(folder)) { + return folder + } + } while (childFolder.fsPath !== folder.fsPath) + return undefined +} From d6232c058be2a96dad9bb0900fc9e6cecd6fc894 Mon Sep 17 00:00:00 2001 From: mhuisi Date: Mon, 25 Sep 2023 14:38:45 +0200 Subject: [PATCH 05/18] incorporate comments --- vscode-lean4/media/guide-documentation.md | 28 ++++++++----------- vscode-lean4/media/guide-help.md | 2 ++ vscode-lean4/media/guide-installElan-unix.md | 2 +- .../media/guide-installElan-windows.md | 2 +- 4 files changed, 15 insertions(+), 19 deletions(-) diff --git a/vscode-lean4/media/guide-documentation.md b/vscode-lean4/media/guide-documentation.md index 0be45e07e..4dd41e272 100644 --- a/vscode-lean4/media/guide-documentation.md +++ b/vscode-lean4/media/guide-documentation.md @@ -1,32 +1,26 @@ ## Books If you want to learn Lean 4, choose one of the following introductory books based on your background. If you are getting stuck or have any questions, click on the 'Questions and Troubleshooting' step below. +- [Functional Programming in Lean](https://lean-lang.org/functional_programming_in_lean/) + The standard introduction for using Lean 4 as a general-purpose programming language. - [Theorem Proving in Lean 4](https://lean-lang.org/theorem_proving_in_lean4/) The standard reference for using Lean 4 as an interactive theorem prover. Suited as an introduction for users with a computer science background, advanced users and for general use as a reference manual. - [Mathematics in Lean](https://leanprover-community.github.io/mathematics_in_lean/) The standard introduction to Lean 4 as an interactive theorem prover for users with a mathematics background. -- [Functional Programming in Lean](https://lean-lang.org/functional_programming_in_lean/) - The standard introduction for using Lean 4 as a general-purpose programming language. +- [The Mechanics of Proof](https://hrmacbeth.github.io/math2001/) + An introduction to Lean 4 as an interactive theorem prover for anyone who also wants to learn how to write rigorous mathematical proofs. -Once you have completed one of these books and its exercises, you are ready to use Lean 4 for your own projects. If you want to use Lean 4 both as an interactive theorem prover and as a general-purpose programming language, it is recommended to read both [Functional Programming in Lean](https://lean-lang.org/functional_programming_in_lean/) and [Theorem Proving in Lean 4](https://lean-lang.org/theorem_proving_in_lean4/). +Once you have completed one of these books and its exercises, you are ready to use Lean 4 for your own projects. If you want to use Lean 4 both as a general-purpose programming language and an interactive theorem prover, it is recommended to read both [Functional Programming in Lean](https://lean-lang.org/functional_programming_in_lean/) and [Theorem Proving in Lean 4](https://lean-lang.org/theorem_proving_in_lean4/). ## Hands-On Tutorial If you want to dive right into using Lean 4 to prove elementary theorems about natural numbers, you can play the [Natural Number Game](https://adam.math.hhu.de/#/g/hhu-adam/NNG4). It can be played online using your browser without a local installation. -## Documentation -**Metaprogramming** -[Metaprogramming in Lean 4](https://github.com/leanprover-community/lean4-metaprogramming-book) is an introduction to the metaprogramming facilities of Lean 4 that can be used to write theorem proving automation, adjust Lean 4 and extend Lean 4. Should only be read after [Functional Programming in Lean](https://lean-lang.org/functional_programming_in_lean/). - -**Type System** -If you are a type theorist and want to learn the details of the type theory underlying Lean 4, you can read [The Type Theory of Lean](https://github.com/digama0/lean-type-theory/releases/download/v1.0/main.pdf) by Mario Carneiro for a formal description of the type theory of Lean 3, which is largely identical to that of Lean 4. The few aspects that have changed between Lean 3 and Lean 4 are described in section 3.2 of [An Extensible Theorem Proving -Frontend](https://lean-lang.org/papers/thesis-sebastian.pdf) by Sebastian Ullrich. +## Additional Resources +**Website** +[Lean's website](https://lean-lang.org/) links to learning resources, publications, talks and articles about Lean. -**Architecture** -Many elements of the architecture of Lean 4, for example its macro system, its do-notation, its reference counting garbage collection and the different components which Lean 4 is composed of are described in [An Extensible Theorem Proving -Frontend](https://lean-lang.org/papers/thesis-sebastian.pdf) by Sebastian Ullrich. +**Lean Community** +The [Lean Community website](https://leanprover-community.github.io/index.html) links to several other helpful learning resources not listed here and provides an introduction to [mathlib](https://github.com/leanprover-community/mathlib4), Lean's math library. **Manual** -The [Lean Manual](https://lean-lang.org/lean4/doc/) is a loose collection of details on Lean 4. - -## Additional Resources -The [Lean Community website](https://leanprover-community.github.io/index.html) links to several other learning resources not listed here that may be helpful. +The [Lean Manual](https://lean-lang.org/lean4/doc/) documents several features of Lean 4 and can be consulted for some of the more technical details concerning Lean. diff --git a/vscode-lean4/media/guide-help.md b/vscode-lean4/media/guide-help.md index 80aecdd83..fc8c93927 100644 --- a/vscode-lean4/media/guide-help.md +++ b/vscode-lean4/media/guide-help.md @@ -4,3 +4,5 @@ To post your question on the [Lean Zulip chat](https://leanprover.zulipchat.com/ 1. [Create a new Lean Zulip chat account](https://leanprover.zulipchat.com/register/). 2. [Visit the #new-members stream](https://leanprover.zulipchat.com/#narrow/stream/113489-new-members). 3. Click the 'New topic' button at the bottom of the page, enter a topic title, describe your question or issue in the message text box and click 'Send'. + +When posting code on the Lean Zulip chat, please reduce the code to a [minimal working example](https://leanprover-community.github.io/mwe.html) that includes all imports and declarations needed for others to copy and paste the code into their own development environment. diff --git a/vscode-lean4/media/guide-installElan-unix.md b/vscode-lean4/media/guide-installElan-unix.md index c17a6a1c9..1acdaceac 100644 --- a/vscode-lean4/media/guide-installElan-unix.md +++ b/vscode-lean4/media/guide-installElan-unix.md @@ -1,4 +1,4 @@ ## Elan [Elan](https://github.com/leanprover/elan) automatically manages all the different versions of Lean and ensures that the correct version is used when opening a project. -Clicking the 'Click to install' button will download the [Elan setup script](https://github.com/leanprover/elan/blob/master/elan-init.sh) and execute it. +Clicking [this link](command:lean4.setup.installElan) will download the [Elan setup script](https://github.com/leanprover/elan/blob/master/elan-init.sh) and execute it. diff --git a/vscode-lean4/media/guide-installElan-windows.md b/vscode-lean4/media/guide-installElan-windows.md index 65fbacf42..c56099c17 100644 --- a/vscode-lean4/media/guide-installElan-windows.md +++ b/vscode-lean4/media/guide-installElan-windows.md @@ -1,4 +1,4 @@ ## Elan [Elan](https://github.com/leanprover/elan) automatically manages all the different versions of Lean and ensures that the correct version is used when opening a project. -Clicking the 'Click to install' button will download the [Elan setup script](https://github.com/leanprover/elan/blob/master/elan-init.ps1) and execute it. +Clicking [this link](command:lean4.setup.installElan) will download the [Elan setup script](https://github.com/leanprover/elan/blob/master/elan-init.ps1) and execute it. From 51e6568a16d13de2c846ffa46b214f598ec8cba9 Mon Sep 17 00:00:00 2001 From: mhuisi Date: Fri, 29 Sep 2023 18:24:46 +0200 Subject: [PATCH 06/18] various quality improvements --- vscode-lean4/media/guide-documentation.md | 8 +- vscode-lean4/package.json | 6 +- vscode-lean4/src/exports.ts | 2 - vscode-lean4/src/extension.ts | 41 ++--- vscode-lean4/src/leanclient.ts | 46 +++--- vscode-lean4/src/project.ts | 184 ++++++++++++++++------ vscode-lean4/src/tasks.ts | 106 ------------- vscode-lean4/src/utils/batch.ts | 160 +++++++++++++++++-- vscode-lean4/src/utils/clientProvider.ts | 10 +- vscode-lean4/src/utils/elan.ts | 5 + vscode-lean4/src/utils/lake.ts | 85 ++++++++++ vscode-lean4/src/utils/leanInstaller.ts | 72 ++------- vscode-lean4/src/utils/projectInfo.ts | 20 +-- 13 files changed, 449 insertions(+), 296 deletions(-) delete mode 100644 vscode-lean4/src/tasks.ts create mode 100644 vscode-lean4/src/utils/elan.ts create mode 100644 vscode-lean4/src/utils/lake.ts diff --git a/vscode-lean4/media/guide-documentation.md b/vscode-lean4/media/guide-documentation.md index 4dd41e272..e5fb82e98 100644 --- a/vscode-lean4/media/guide-documentation.md +++ b/vscode-lean4/media/guide-documentation.md @@ -3,12 +3,12 @@ If you want to learn Lean 4, choose one of the following introductory books base - [Functional Programming in Lean](https://lean-lang.org/functional_programming_in_lean/) The standard introduction for using Lean 4 as a general-purpose programming language. -- [Theorem Proving in Lean 4](https://lean-lang.org/theorem_proving_in_lean4/) - The standard reference for using Lean 4 as an interactive theorem prover. Suited as an introduction for users with a computer science background, advanced users and for general use as a reference manual. -- [Mathematics in Lean](https://leanprover-community.github.io/mathematics_in_lean/) - The standard introduction to Lean 4 as an interactive theorem prover for users with a mathematics background. - [The Mechanics of Proof](https://hrmacbeth.github.io/math2001/) An introduction to Lean 4 as an interactive theorem prover for anyone who also wants to learn how to write rigorous mathematical proofs. +- [Mathematics in Lean](https://leanprover-community.github.io/mathematics_in_lean/) + The standard introduction to Lean 4 as an interactive theorem prover for users with a mathematics background. +- [Theorem Proving in Lean 4](https://lean-lang.org/theorem_proving_in_lean4/) + The standard reference for using Lean 4 as an interactive theorem prover. Suited as an introduction for users with a computer science background, advanced users and for general use as a reference manual. Once you have completed one of these books and its exercises, you are ready to use Lean 4 for your own projects. If you want to use Lean 4 both as a general-purpose programming language and an interactive theorem prover, it is recommended to read both [Functional Programming in Lean](https://lean-lang.org/functional_programming_in_lean/) and [Theorem Proving in Lean 4](https://lean-lang.org/theorem_proving_in_lean4/). diff --git a/vscode-lean4/package.json b/vscode-lean4/package.json index 3317a82cf..754f47ccc 100644 --- a/vscode-lean4/package.json +++ b/vscode-lean4/package.json @@ -692,7 +692,7 @@ { "id": "guide.linux.openSetupGuide", "title": "Re-Open Setup Guide", - "description": "This guide can always be re-opened by clicking on the ∀-symbol in the top right and selecting 'Show Documentation…' > 'Setup: Show Setup Guide'.", + "description": "This guide can always be re-opened by opening an empty file, clicking on the ∀-symbol in the top right and selecting 'Show Documentation…' > 'Setup: Show Setup Guide'.", "media": { "image": "media/open-setup-guide.png", "altText": "Click on the ∀-symbol in the top right and select 'Show Documentation…' > 'Setup: Show Setup Guide'." @@ -739,7 +739,7 @@ { "id": "guide.mac.openSetupGuide", "title": "Re-Open Setup Guide", - "description": "This guide can always be re-opened by clicking on the ∀-symbol in the top right and selecting 'Show Documentation…' > 'Setup: Show Setup Guide'.", + "description": "This guide can always be re-opened by opening an empty file, clicking on the ∀-symbol in the top right and selecting 'Show Documentation…' > 'Setup: Show Setup Guide'.", "media": { "image": "media/open-setup-guide.png", "altText": "Click on the ∀-symbol in the top right and select 'Show Documentation…' > 'Setup: Show Setup Guide'." @@ -786,7 +786,7 @@ { "id": "guide.windows.openSetupGuide", "title": "Re-Open Setup Guide", - "description": "This guide can always be re-opened by clicking on the ∀-symbol in the top right and selecting 'Show Documentation…' > 'Setup: Show Setup Guide'.", + "description": "This guide can always be re-opened by opening an empty file, clicking on the ∀-symbol in the top right and selecting 'Show Documentation…' > 'Setup: Show Setup Guide'.", "media": { "image": "media/open-setup-guide.png", "altText": "Click on the ∀-symbol in the top right and select 'Show Documentation…' > 'Setup: Show Setup Guide'." diff --git a/vscode-lean4/src/exports.ts b/vscode-lean4/src/exports.ts index e5e98c382..a094bf42d 100644 --- a/vscode-lean4/src/exports.ts +++ b/vscode-lean4/src/exports.ts @@ -2,7 +2,6 @@ import { InfoProvider } from './infoview' import { DocViewProvider } from './docview'; import { LeanInstaller } from './utils/leanInstaller' import { LeanClientProvider } from './utils/clientProvider'; -import { LeanTaskProvider } from './tasks'; import { ProjectOperationProvider } from './project'; export interface Exports { @@ -12,6 +11,5 @@ export interface Exports { clientProvider: LeanClientProvider | undefined installer: LeanInstaller | undefined docView: DocViewProvider | undefined - taskProvider: LeanTaskProvider | undefined projectOperationProvider: ProjectOperationProvider | undefined } diff --git a/vscode-lean4/src/extension.ts b/vscode-lean4/src/extension.ts index 8c32949f9..ff895019d 100644 --- a/vscode-lean4/src/extension.ts +++ b/vscode-lean4/src/extension.ts @@ -1,21 +1,19 @@ -import { window, ExtensionContext, TextDocument, tasks, commands } from 'vscode' +import { window, ExtensionContext, TextDocument, tasks, commands, Disposable } from 'vscode' import { AbbreviationFeature } from './abbreviation' import { InfoProvider } from './infoview' -import { DocViewProvider } from './docview'; +import { DocViewProvider } from './docview' import { LeanTaskGutter } from './taskgutter' import { LeanInstaller } from './utils/leanInstaller' -import { LeanpkgService } from './utils/leanpkg'; -import { LeanClientProvider } from './utils/clientProvider'; -import { addDefaultElanPath, removeElanPath, addToolchainBinPath, isElanDisabled, getDefaultLeanVersion} from './config'; -import { findLeanPackageVersionInfo } from './utils/projectInfo'; +import { LeanpkgService } from './utils/leanpkg' +import { LeanClientProvider } from './utils/clientProvider' +import { addDefaultElanPath, removeElanPath, addToolchainBinPath, isElanDisabled, getDefaultLeanVersion} from './config' +import { findLeanPackageVersionInfo } from './utils/projectInfo' import { Exports } from './exports'; -import { LeanTaskProvider, leanTaskDefinition } from './tasks'; import { logger } from './utils/logger' -import { ProjectOperationProvider } from './project'; +import { ProjectOperationProvider } from './project' interface AlwaysEnabledFeatures { docView: DocViewProvider - taskProvider: LeanTaskProvider projectOperationProvider: ProjectOperationProvider installer: LeanInstaller } @@ -74,11 +72,6 @@ function activateAlwaysEnabledFeatures(context: ExtensionContext): AlwaysEnabled const docView = new DocViewProvider(context.extensionUri); context.subscriptions.push(docView); - // safe - const taskProvider = new LeanTaskProvider() - context.subscriptions.push(tasks.registerTaskProvider(leanTaskDefinition.type, taskProvider)) - - // safe const projectOperationProvider = new ProjectOperationProvider() context.subscriptions.push(projectOperationProvider) @@ -86,7 +79,7 @@ function activateAlwaysEnabledFeatures(context: ExtensionContext): AlwaysEnabled const defaultToolchain = getDefaultLeanVersion(); const installer = new LeanInstaller(outputChannel, defaultToolchain) - return { docView, taskProvider, projectOperationProvider, installer } + return { docView, projectOperationProvider, installer } } async function isLean3Project(installer: LeanInstaller): Promise { @@ -117,18 +110,16 @@ function activateAbbreviationFeature(context: ExtensionContext, docView: DocView return abbrev } -function activateLean4Features(context: ExtensionContext, installer: LeanInstaller): Lean4EnabledFeatures { - // unsafe +function activateLean4Features(context: ExtensionContext, installer: LeanInstaller, projectOperationProvider: ProjectOperationProvider): Lean4EnabledFeatures { const pkgService = new LeanpkgService() pkgService.versionChanged((uri) => installer.handleVersionChanged(uri)); pkgService.lakeFileChanged((uri) => installer.handleLakeFileChanged(uri)); context.subscriptions.push(pkgService); - // unsafe const clientProvider = new LeanClientProvider(installer, pkgService, installer.getOutputChannel()); + projectOperationProvider.clientProvider = clientProvider context.subscriptions.push(clientProvider) - // unsafe const infoProvider = new InfoProvider(clientProvider, {language: 'lean4'}, context); context.subscriptions.push(infoProvider) @@ -148,7 +139,6 @@ export async function activate(context: ExtensionContext): Promise { clientProvider: undefined, installer: alwaysEnabledFeatures.installer, docView: alwaysEnabledFeatures.docView, - taskProvider: alwaysEnabledFeatures.taskProvider, projectOperationProvider: alwaysEnabledFeatures.projectOperationProvider } } @@ -156,7 +146,7 @@ export async function activate(context: ExtensionContext): Promise { activateAbbreviationFeature(context, alwaysEnabledFeatures.docView) if (findOpenLeanDocument()) { - const lean4EnabledFeatures: Lean4EnabledFeatures = activateLean4Features(context, alwaysEnabledFeatures.installer) + const lean4EnabledFeatures: Lean4EnabledFeatures = activateLean4Features(context, alwaysEnabledFeatures.installer, alwaysEnabledFeatures.projectOperationProvider) return { isLean4Project: true, version: '4', @@ -164,17 +154,17 @@ export async function activate(context: ExtensionContext): Promise { clientProvider: lean4EnabledFeatures.clientProvider, installer: alwaysEnabledFeatures.installer, docView: alwaysEnabledFeatures.docView, - taskProvider: alwaysEnabledFeatures.taskProvider, projectOperationProvider: alwaysEnabledFeatures.projectOperationProvider } } // No Lean 4 document yet => Load remaining features when one is open - window.onDidChangeVisibleTextEditors(_ => { + const disposeActivationListener: Disposable = window.onDidChangeVisibleTextEditors(_ => { if (findOpenLeanDocument()) { - activateLean4Features(context, alwaysEnabledFeatures.installer) + activateLean4Features(context, alwaysEnabledFeatures.installer, alwaysEnabledFeatures.projectOperationProvider) } - }) + disposeActivationListener.dispose() + }, context.subscriptions) return { isLean4Project: false, @@ -183,7 +173,6 @@ export async function activate(context: ExtensionContext): Promise { clientProvider: undefined, installer: alwaysEnabledFeatures.installer, docView: alwaysEnabledFeatures.docView, - taskProvider: alwaysEnabledFeatures.taskProvider, projectOperationProvider: alwaysEnabledFeatures.projectOperationProvider } } diff --git a/vscode-lean4/src/leanclient.ts b/vscode-lean4/src/leanclient.ts index 1e1aa8dab..19200b676 100644 --- a/vscode-lean4/src/leanclient.ts +++ b/vscode-lean4/src/leanclient.ts @@ -20,7 +20,7 @@ import * as ls from 'vscode-languageserver-protocol' import { toolchainPath, lakePath, addServerEnvPaths, serverArgs, serverLoggingEnabled, serverLoggingPath, shouldAutofocusOutput, getElaborationDelay, lakeEnabled } from './config' import { assert } from './utils/assert' import { LeanFileProgressParams, LeanFileProgressProcessingInfo, ServerStoppedReason } from '@leanprover/infoview-api'; -import { batchExecute } from './utils/batch' +import { ExecutionExitCode, ExecutionResult, batchExecute } from './utils/batch' import { readLeanVersion } from './utils/projectInfo'; import * as fs from 'fs'; import { URL } from 'url'; @@ -106,25 +106,26 @@ export class LeanClient implements Disposable { } async showRestartMessage(restartFile: boolean = false): Promise { - if (!this.showingRestartMessage) { - this.showingRestartMessage = true; - let restartItem :string; - let messageTitle :string; - if (!restartFile) { - restartItem = 'Restart Lean Server'; - messageTitle = 'Lean Server has stopped unexpectedly.' + if (this.showingRestartMessage) { + return + } + this.showingRestartMessage = true; + let restartItem: string; + let messageTitle: string; + if (!restartFile) { + restartItem = 'Restart Lean Server'; + messageTitle = 'Lean Server has stopped unexpectedly.' + } else { + restartItem = 'Restart Lean Server on this file'; + messageTitle = 'The Lean Server has stopped processing this file.' + } + const item = await window.showErrorMessage(messageTitle, restartItem) + this.showingRestartMessage = false; + if (item === restartItem) { + if (restartFile && window.activeTextEditor) { + await this.restartFile(window.activeTextEditor.document); } else { - restartItem = 'Restart Lean Server on this file'; - messageTitle = 'The Lean Server has stopped processing this file.' - } - const item = await window.showErrorMessage(messageTitle, restartItem) - this.showingRestartMessage = false; - if (item === restartItem) { - if (restartFile && window.activeTextEditor){ - await this.restartFile(window.activeTextEditor.document); - } else { - void this.start(); - } + void this.start(); } } } @@ -433,7 +434,6 @@ export class LeanClient implements Disposable { else { return uri.scheme === 'untitled' } - return false; } getWorkspaceFolder() : string { @@ -562,8 +562,12 @@ export class LeanClient implements Disposable { // Check that the Lake version is high enough to support "lake serve" option. const versionOptions = version ? ['+' + version, '--version'] : ['--version'] const start = Date.now() - const lakeVersion = await batchExecute(executable, versionOptions, this.folderUri?.fsPath, undefined); + const result: ExecutionResult = await batchExecute(executable, versionOptions, this.folderUri?.fsPath, undefined); + if (result.exitCode !== ExecutionExitCode.Success) { + return false + } logger.log(`[LeanClient] Ran '${executable} ${versionOptions.join(' ')}' in ${Date.now() - start} ms`); + const lakeVersion = result.stdout const actual = this.extractVersion(lakeVersion) if (actual.compare('3.0.0') > 0) { return true; diff --git a/vscode-lean4/src/project.ts b/vscode-lean4/src/project.ts index 3a7a3c4a1..837ecccb1 100644 --- a/vscode-lean4/src/project.ts +++ b/vscode-lean4/src/project.ts @@ -1,12 +1,16 @@ -import * as vscode from 'vscode'; -import { Disposable, TaskRevealKind, Uri, commands, window, workspace, SaveDialogOptions, FileType } from 'vscode'; -import { LeanTask, buildTask, cacheGetTask, cleanTask, createExecutableTask, runTaskUntilCompletion, updateElanTask, updateTask } from './tasks'; +import { Disposable, Uri, commands, window, workspace, SaveDialogOptions } from 'vscode'; import path = require('path'); import { checkParentFoldersForLeanProject, isValidLeanProject } from './utils/projectInfo'; +import { elanSelfUpdate } from './utils/elan'; +import { LakeRunner, cacheNotFoundError, lake, lakeInActiveFolder } from './utils/lake'; +import { ExecutionExitCode, ExecutionResult, batchExecute, batchExecuteWithProgress, displayError } from './utils/batch'; +import { LeanClientProvider } from './utils/clientProvider'; +import { LeanClient } from './leanclient'; export class ProjectOperationProvider implements Disposable { private subscriptions: Disposable[] = []; + clientProvider: LeanClientProvider | undefined = undefined // set when the lean 4 client loads constructor() { this.subscriptions.push( @@ -22,55 +26,69 @@ export class ProjectOperationProvider implements Disposable { } private async createLibraryProject() { - await this.createProject('lib', 'library', 'stable') + const projectFolder: Uri | 'DidNotComplete' = await this.createProject('lib', 'library', 'stable') + + if (projectFolder !== 'DidNotComplete') { + await ProjectOperationProvider.openNewFolder(projectFolder) + } } private async createProgramProject() { - await this.createProject('exe', 'program', 'stable') + const projectFolder: Uri | 'DidNotComplete' = await this.createProject('exe', 'program', 'stable') + + if (projectFolder !== 'DidNotComplete') { + await ProjectOperationProvider.openNewFolder(projectFolder) + } } private async createMathlibProject() { - await this.createProject('math', 'math formalization', - 'leanprover-community/mathlib4:lean-toolchain', - [updateTask, cacheGetTask]) + const mathlibToolchain = 'leanprover-community/mathlib4:lean-toolchain' + const projectFolder: Uri | 'DidNotComplete' = await this.createProject('math', 'math formalization', mathlibToolchain) + + if (projectFolder === 'DidNotComplete') { + return + } + + const updateResult: ExecutionResult = await lake(projectFolder, mathlibToolchain).updateDependencies() + if (updateResult.exitCode !== ExecutionExitCode.Success) { + await displayError(updateResult, 'Cannot update dependencies.') + return + } + + const cacheGetResult: ExecutionResult = await lake(projectFolder, mathlibToolchain).fetchMathlibCache() + if (cacheGetResult.exitCode !== ExecutionExitCode.Success) { + await displayError(cacheGetResult, 'Cannot fetch Mathlib build artifact cache.') + return + } + + await ProjectOperationProvider.openNewFolder(projectFolder) } private async createProject( kind: string, kindName: string, - toolchain?: string | undefined, - postProcessingTasks: LeanTask[] = []) { + toolchain?: string | undefined): Promise { const projectFolder: Uri | undefined = await ProjectOperationProvider.askForNewProjectFolderLocation({ saveLabel: 'Create project folder', title: `Create a new ${kindName} project folder` }) if (projectFolder === undefined) { - return + return 'DidNotComplete' } await workspace.fs.createDirectory(projectFolder) + await elanSelfUpdate() + const projectName: string = path.basename(projectFolder.fsPath) - const initCommand: string = - toolchain === undefined - ? 'init' - : `+${toolchain} init` - const createProjectTask: LeanTask = { - command: `lake ${initCommand} "${projectName}" ${kind}`, - description: `Create new Lean 4 ${kindName} project` - } - - const tasks = [updateElanTask, createProjectTask].concat(postProcessingTasks) - for (const task of tasks) { - try { - await runTaskUntilCompletion(createExecutableTask(task, TaskRevealKind.Always, projectFolder.fsPath), this.subscriptions) - } catch (e) { - return // Error will already be displayed in terminal - } + const result: ExecutionResult = await lake(projectFolder, toolchain).initProject(projectName, kind) + if (result.exitCode !== ExecutionExitCode.Success) { + await displayError(result, 'Cannot initialize project.') + return 'DidNotComplete' } - await ProjectOperationProvider.openNewFolder(projectFolder) + return projectFolder } private async openProject() { @@ -105,7 +123,7 @@ export class ProjectOperationProvider implements Disposable { private static async attemptFindingLeanProjectInParentFolder(projectFolder: Uri): Promise { const parentProjectFolder: Uri | undefined = await checkParentFoldersForLeanProject(projectFolder) if (parentProjectFolder === undefined) { - await window.showErrorMessage('The selected folder is not a valid Lean 4 project folder. Please make sure to select a folder containing a \'lean-toolchain\' file.') + void window.showErrorMessage('The selected folder is not a valid Lean 4 project folder. Please make sure to select a folder containing a \'lean-toolchain\' file.') return undefined } @@ -148,15 +166,15 @@ Open this project instead?` return } - try { - await runTaskUntilCompletion(createExecutableTask({ - command: `git clone "${existingProjectUri}" "${projectFolder.fsPath}"`, - description: 'Download existing Lean 4 project using `git clone`' - }), this.subscriptions) - } catch (e) { - return // Error will already be displayed in terminal + const result: ExecutionResult = await batchExecuteWithProgress('git', ['clone', existingProjectUri.toString(), projectFolder.fsPath], 'Cloning project ...') + if (result.exitCode !== ExecutionExitCode.Success) { + await displayError(result, 'Cannot download project.') + return } + // Try it. If this is not a mathlib project, it will fail silently. Otherwise, it will grab the cache. + await lake(projectFolder).fetchMathlibCache(true) + await ProjectOperationProvider.openNewFolder(projectFolder) } @@ -172,7 +190,7 @@ Open this project instead?` if (projectFolder.scheme === 'file') { return true } else { - await window.showErrorMessage('Project folder must be created in a file system.') + void window.showErrorMessage('Project folder must be created in a file system.') return false } } @@ -188,20 +206,98 @@ Open this project instead?` } private async buildProject() { - await vscode.tasks.executeTask(createExecutableTask(buildTask)) + await this.inActiveFolderWithoutServer(async lakeRunner => { + // Try it. If this is not a mathlib project, it will fail silently. Otherwise, it will grab the cache. + await lakeRunner.fetchMathlibCache(true) + + const result: ExecutionResult = await lakeRunner.build() + if (result.exitCode !== ExecutionExitCode.Success) { + void displayError(result, 'Cannot build project.') + return + } + + void window.showInformationMessage('Project built successfully.') + return + }) } private async cleanProject() { - const input = 'Proceed' - const choice: string | undefined = await window.showInformationMessage('Delete all build artifacts?', { modal: true }, input) - - if (choice === input) { - await vscode.tasks.executeTask(createExecutableTask(cleanTask)) + const deleteInput = 'Proceed' + const deleteChoice: string | undefined = await window.showInformationMessage('Delete all build artifacts?', { modal: true }, deleteInput) + if (deleteChoice !== deleteInput) { + return } + + await this.inActiveFolderWithoutServer(async lakeRunner => { + const cleanResult: ExecutionResult = await lakeRunner.clean() + if (cleanResult.exitCode !== ExecutionExitCode.Success) { + void displayError(cleanResult, 'Cannot delete build artifacts.') + return + } + + if (!await lakeRunner.isMathlibCacheGetAvailable()) { + void window.showInformationMessage('Project cleaned successfully.') + return + } + + const fetchMessage = 'Project cleaned successfully. Do you want to fetch Mathlib\'s build artifact cache?' + const fetchInput = 'Fetch Cache' + const fetchChoice: string | undefined = await window.showInformationMessage(fetchMessage, { modal: true }, fetchInput) + if (fetchChoice !== fetchInput) { + return + } + + const fetchResult: ExecutionResult = await lakeRunner.fetchMathlibCache() + if (fetchResult.exitCode !== ExecutionExitCode.Success) { + void displayError(fetchResult, 'Cannot fetch Mathlib build artifact cache.') + return + } + void window.showInformationMessage('Mathlib build artifact cache fetched successfully.') + }) } private async fetchMathlibCache() { - await vscode.tasks.executeTask(createExecutableTask(cacheGetTask)) + await this.inActiveFolderWithoutServer(async lakeRunner => { + const result: ExecutionResult = await lakeRunner.fetchMathlibCache() + if (result.exitCode !== ExecutionExitCode.Success) { + if (result.stderr.includes(cacheNotFoundError)) { + void window.showErrorMessage('This command cannot be used in non-Mathlib projects.') + return + } + void displayError(result, 'Cannot fetch Mathlib build artifact cache.') + return + } + + void window.showInformationMessage('Mathlib build artifact cache fetched successfully.') + }) + } + + private async inActiveFolderWithoutServer(command: (lakeRunner: LakeRunner) => Promise) { + if (!this.clientProvider) { + void window.showErrorMessage('Lean client has not been loaded yet.') + return + } + + const lakeRunner: LakeRunner | 'NoActiveFolder' = await lakeInActiveFolder() + if (lakeRunner === 'NoActiveFolder') { + return + } + + const activeClient: LeanClient | undefined = this.clientProvider.getActiveClient() + if (!activeClient) { + void window.showErrorMessage('No active client.') + return + } + + if (activeClient.isRunning()) { + await activeClient.stop() + } + + await command(lakeRunner) + + if (!activeClient.isRunning()) { + await activeClient.start() + } } dispose() { diff --git a/vscode-lean4/src/tasks.ts b/vscode-lean4/src/tasks.ts deleted file mode 100644 index bca5706a7..000000000 --- a/vscode-lean4/src/tasks.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { CancellationToken, ProviderResult, ShellExecution, Task, TaskProvider, TaskScope, TaskDefinition, TaskRevealKind, tasks, Disposable } from 'vscode' - -export const leanTaskDefinition: TaskDefinition = { type: 'lean4' } - -export interface LeanTask { - command: string - description: string -} - -export function createExecutableTask(task: LeanTask, reveal: TaskRevealKind = TaskRevealKind.Always, cwd?: string | undefined): Task { - // Use `process.env` because if users just installed elan, the default parent process env - // of the task will not contain the elan executables, while `process.env` does - const env = Object.entries(process.env) - .filter(([_, value]) => value !== undefined) - .reduce((obj, [key, value]) => { - obj[key] = value as string; - return obj; - }, {} as {[key: string]: string}); - - const t = new Task( - leanTaskDefinition, - TaskScope.Workspace, - task.description, - 'Lean 4', - new ShellExecution(task.command, { cwd, env }), - '' - ) - t.presentationOptions.reveal = reveal - return t -} - -export async function runTaskUntilCompletion(task: Task, subscriptions: Disposable[]): Promise { - const execution = await tasks.executeTask(task) - return new Promise((resolve, reject) => { - tasks.onDidEndTaskProcess(async e => { - if (e.execution !== execution) { - return - } - - if (e.exitCode === 0) { - resolve() - } else { - reject(e.exitCode) - } - }, undefined, subscriptions) - }) -} - -export const updateElanTask: LeanTask = { - command: 'elan self update', - description: 'Update Lean\'s version manager Elan' -} -export const initLibraryProjectTask: LeanTask = { - command: 'lake +stable init ${workspaceFolderBasename} lib', - description: 'Initialize Lean 4 library project in current folder' -} -export const initProgramProjectTask: LeanTask = { - command: 'lake +stable init ${workspaceFolderBasename} exe', - description: 'Initialize Lean 4 program project in current folder' -} -export const initMathlibProjectTask: LeanTask = { - command: 'lake +leanprover-community/mathlib4:lean-toolchain init ${workspaceFolderBasename} math', - description: 'Initialize Lean 4 math formalization project in current folder' -} -export const buildTask: LeanTask = { - command: 'lake build', - description: 'Build Lean 4 project' -} -export const cleanTask: LeanTask = { - command: 'lake clean', - description: 'Clean build artifacts of Lean 4 project' -} -export const cacheGetTask: LeanTask = { - command: 'lake exe cache get', - description: '⚠ Mathlib command ⚠: Download cached Mathlib build artifacts' -} -export const cachePackTask: LeanTask = { - command: 'lake exe cache pack', - description: '⚠ Mathlib command ⚠: Compress and cache local build artifacts' -} -export const updateTask: LeanTask = { - command: 'lake update', - description: '⚠ Project maintenance command ⚠: Upgrade all project dependencies' -} - -export class LeanTaskProvider implements TaskProvider { - - provideTasks(token: CancellationToken): ProviderResult { - return [ - createExecutableTask(updateElanTask), - createExecutableTask(initLibraryProjectTask, TaskRevealKind.Silent), - createExecutableTask(initProgramProjectTask, TaskRevealKind.Silent), - createExecutableTask(initMathlibProjectTask, TaskRevealKind.Silent), - createExecutableTask(buildTask), - createExecutableTask(cleanTask), - createExecutableTask(cacheGetTask), - createExecutableTask(cachePackTask), - createExecutableTask(updateTask) - ] - } - - resolveTask(task: Task, token: CancellationToken): ProviderResult { - return undefined - } - -} diff --git a/vscode-lean4/src/utils/batch.ts b/vscode-lean4/src/utils/batch.ts index 3f962ad76..474c03b19 100644 --- a/vscode-lean4/src/utils/batch.ts +++ b/vscode-lean4/src/utils/batch.ts @@ -1,16 +1,43 @@ -import { OutputChannel } from 'vscode' +import { OutputChannel, ProgressLocation, ProgressOptions, window } from 'vscode' import { spawn } from 'child_process'; import { findProgramInPath, isRunningTest } from '../config' import { logger } from './logger' +export interface ExecutionChannel { + combined?: OutputChannel | undefined + stdout?: OutputChannel | undefined + stderr?: OutputChannel | undefined +} + +export enum ExecutionExitCode { + Success, + CannotLaunch, + ExecutionError +} + +export interface ExecutionResult { + exitCode: ExecutionExitCode + stdout: string + stderr: string +} + +function createCannotLaunchExecutionResult(message: string): ExecutionResult { + return { + exitCode: ExecutionExitCode.CannotLaunch, + stdout: '', + stderr: message + } +} + export async function batchExecute( executablePath: string, - args: any[], - workingDirectory: string | null, - channel: OutputChannel | undefined): Promise { + args: string[], + workingDirectory?: string | undefined, + channel?: ExecutionChannel | undefined): Promise { - return new Promise(function(resolve, reject){ - let output : string = ''; + return new Promise(function(resolve, reject) { + let stdout: string = '' + let stderr: string = '' let options = {} if (workingDirectory !== undefined) { options = { cwd: workingDirectory }; @@ -25,37 +52,136 @@ export async function batchExecute( // check if the command exists so as not to trigger that exception. const fullPath = findProgramInPath(executablePath); if (!fullPath) { - resolve(undefined); + resolve(createCannotLaunchExecutionResult('')); return; } } const proc = spawn(executablePath, args, options); - if (proc.pid === undefined) { - resolve(undefined); - return; - } + proc.on('error', err => { + resolve(createCannotLaunchExecutionResult(err.message)) + }); proc.stdout.on('data', (line) => { const s: string = line.toString(); - if (channel) channel.appendLine(s); - output += s + '\n'; + if (channel && channel.combined) channel.combined.appendLine(s) + if (channel && channel.stdout) channel.stdout.appendLine(s) + stdout += s + '\n'; }); proc.stderr.on('data', (line) => { const s: string = line.toString(); - if (channel) channel.appendLine(s); - output += s + '\n'; + if (channel && channel.combined) channel.combined.appendLine(s) + if (channel && channel.stderr) channel.stderr.appendLine(s) + stderr += s + '\n'; }); proc.on('close', (code) => { logger.log(`child process exited with code ${code}`); - resolve(output) + if (code !== 0) { + resolve({ + exitCode: ExecutionExitCode.ExecutionError, + stdout, + stderr + }) + return + } + resolve({ + exitCode: ExecutionExitCode.Success, + stdout, + stderr + }) }); } catch (e){ logger.log(`error running ${executablePath} : ${e}`); - resolve(undefined); + resolve(createCannotLaunchExecutionResult('')); + } + }); +} + +export async function batchExecuteWithProgress( + executablePath: string, + args: string[], + prompt: string, + workingDirectory?: string | undefined, + channel?: OutputChannel | undefined, + translator?: ((line: string) => string | undefined) | undefined): Promise { + + const progressOptions: ProgressOptions = { + location: ProgressLocation.Notification, + title: '', + cancellable: false + } + let inc = 0 + + const result: ExecutionResult = await window.withProgress(progressOptions, progress => { + const progressChannel: OutputChannel = { + name : 'ProgressChannel', + append(value: string) { + if (translator) { + const translatedValue: string | undefined = translator(value) + if (translatedValue === undefined) { + return + } + value = translatedValue + } + if (channel) { + channel.appendLine(value) + } + if (inc < 90) { + inc += 2 + } + progress.report({ increment: inc, message: value }) + }, + appendLine(value: string) { + this.append(value + '\n') + }, + replace(_: string) { /* empty */ }, + clear() { /* empty */ }, + show() { /* empty */ }, + hide() { /* empty */ }, + dispose() { /* empty */ } } + progress.report({ increment: 0, message: prompt }); + return batchExecute(executablePath, args, workingDirectory, { combined: progressChannel }); }); + return result; +} + +type ExecutionHandler = () => Promise + +export interface BatchExecution { + execute: ExecutionHandler + optional?: boolean | undefined // `false` by default +} + +export async function executeAll(executions: BatchExecution[]): Promise { + const results: ExecutionResult[] = [] + for (const execution of executions) { + const result: ExecutionResult = await execution.execute() + results.push(result) + if (execution.optional !== true && result.exitCode !== ExecutionExitCode.Success) { + break + } + } + return results +} + +export async function displayError(result: ExecutionResult, message: string, modal: boolean = false) { + if (result.exitCode === ExecutionExitCode.Success) { + throw Error() + } + const errorMessage: string = formatErrorMessage(result, message) + await window.showErrorMessage(errorMessage, { modal }) +} + +function formatErrorMessage(error: ExecutionResult, message: string): string { + if (error.stderr === '') { + return `${message}` + } + return `${message} + +Command error output: +${error.stderr}` } diff --git a/vscode-lean4/src/utils/clientProvider.ts b/vscode-lean4/src/utils/clientProvider.ts index fe1763cba..56bb4eb4a 100644 --- a/vscode-lean4/src/utils/clientProvider.ts +++ b/vscode-lean4/src/utils/clientProvider.ts @@ -1,4 +1,4 @@ -import { Disposable, OutputChannel, workspace, TextDocument, commands, window, EventEmitter, Uri, languages, TextEditor } from 'vscode'; +import { Disposable, OutputChannel, workspace, TextDocument, commands, window, EventEmitter, Uri, languages, TextEditor, WorkspaceFolder } from 'vscode'; import { LeanInstaller, LeanVersion } from './leanInstaller' import { LeanpkgService } from './leanpkg'; import { LeanClient } from '../leanclient' @@ -90,7 +90,7 @@ export class LeanClientProvider implements Disposable { { try { const uri = this.pendingInstallChanged.pop(); - if (uri){ + if (uri) { // have to check again here in case elan install had --default-toolchain none. const [workspaceFolder, folder, packageFileUri] = await findLeanPackageRoot(uri); const packageUri = folder ? folder : Uri.from({scheme: 'untitled'}); @@ -122,6 +122,10 @@ export class LeanClientProvider implements Disposable { } else { addDefaultElanPath(); } + + for (const [_, client] of this.clients) { + await this.onInstallChanged(client.folderUri) + } } private getVisibleEditor(uri: Uri) : TextEditor | null { @@ -232,7 +236,7 @@ export class LeanClientProvider implements Disposable { } getClientForFolder(folder: Uri) : LeanClient | undefined { - let client: LeanClient | undefined; + let client: LeanClient | undefined; const key = this.getKeyFromUri(folder); const cachedClient = this.clients.has(key); if (cachedClient) { diff --git a/vscode-lean4/src/utils/elan.ts b/vscode-lean4/src/utils/elan.ts new file mode 100644 index 000000000..2e44ee978 --- /dev/null +++ b/vscode-lean4/src/utils/elan.ts @@ -0,0 +1,5 @@ +import { ExecutionResult, batchExecute } from './batch'; + +export async function elanSelfUpdate(): Promise { + return await batchExecute('elan', ['self', 'update']) +} diff --git a/vscode-lean4/src/utils/lake.ts b/vscode-lean4/src/utils/lake.ts new file mode 100644 index 000000000..999877d7b --- /dev/null +++ b/vscode-lean4/src/utils/lake.ts @@ -0,0 +1,85 @@ +import { Uri, window } from 'vscode'; +import { ExecutionExitCode, ExecutionResult, batchExecute, batchExecuteWithProgress } from './batch'; +import { findLeanPackageRoot } from './projectInfo'; + +export const cacheNotFoundError = 'unknown executable `cache`' + +export class LakeRunner { + cwdUri: Uri | undefined + toolchain: string | undefined + + constructor(cwdUri: Uri | undefined, toolchain?: string | undefined) { + this.cwdUri = cwdUri + this.toolchain = toolchain + } + + async initProject(name: string, kind: string): Promise { + return this.runLakeCommandSilently('init', [name, kind]) + } + + async updateDependencies(): Promise { + return this.runLakeCommandWithProgress('update', [], 'Updating dependencies ...') + } + + async build(): Promise { + return this.runLakeCommandWithProgress('build', [], 'Building Lean project ...') + } + + async clean(): Promise { + return this.runLakeCommandWithProgress('clean', [], 'Cleaning Lean project ...') + } + + async fetchMathlibCache(filterError: boolean = false): Promise { + const prompt = 'Checking whether Mathlib build artifact cache needs to be downloaded ...' + return this.runLakeCommandWithProgress('exe', ['cache', 'get'], prompt, line => { + if (filterError && line.includes(cacheNotFoundError)) { + return undefined + } + return line + }) + } + + async isMathlibCacheGetAvailable(): Promise { + const result: ExecutionResult = await this.runLakeCommandSilently('exe', ['cache']) + return result.exitCode === ExecutionExitCode.Success + } + + private async runLakeCommandSilently(subCommand: string, args: string[]): Promise { + args = args.slice() + args.unshift(subCommand) + if (this.toolchain) { + args.unshift(`+${this.toolchain}`) + } + return await batchExecute('lake', args, this.cwdUri?.fsPath) + } + + private async runLakeCommandWithProgress( + subCommand: string, + args: string[], + waitingPrompt: string, + translator?: ((line: string) => string | undefined) | undefined): Promise { + args = args.slice() + args.unshift(subCommand) + if (this.toolchain) { + args.unshift(`+${this.toolchain}`) + } + return await batchExecuteWithProgress('lake', args, waitingPrompt, this.cwdUri?.fsPath, undefined, translator) + } +} + +export function lake(cwdUri: Uri | undefined, toolchain?: string | undefined): LakeRunner { + return new LakeRunner(cwdUri, toolchain) +} + +export async function lakeInActiveFolder(toolchain?: string | undefined): Promise { + if (!window.activeTextEditor) { + return 'NoActiveFolder' + } + + const [_1, folderUri, _2] = await findLeanPackageRoot(window.activeTextEditor.document.uri) + if (!folderUri) { + return 'NoActiveFolder' + } + + return lake(folderUri, toolchain) +} diff --git a/vscode-lean4/src/utils/leanInstaller.ts b/vscode-lean4/src/utils/leanInstaller.ts index 2ef383d6f..6c3478251 100644 --- a/vscode-lean4/src/utils/leanInstaller.ts +++ b/vscode-lean4/src/utils/leanInstaller.ts @@ -1,6 +1,6 @@ import { window, TerminalOptions, OutputChannel, Disposable, EventEmitter, ProgressLocation, Uri } from 'vscode' import { toolchainPath, addServerEnvPaths, getPowerShellPath, shouldAutofocusOutput, isRunningTest } from '../config' -import { batchExecute } from './batch' +import { ExecutionExitCode, ExecutionResult, batchExecute, batchExecuteWithProgress } from './batch' import { readLeanVersion, isCoreLean4Directory } from './projectInfo'; import { join } from 'path'; import { logger } from './logger' @@ -224,18 +224,16 @@ export class LeanInstaller { // looks for a global (default) installation of Lean. This way, we can support // single file editing. logger.log(`executeWithProgress ${cmd} ${options}`) - const stdout = await this.executeWithProgress('Checking Lean setup...', cmd, options, folderPath) - if (!stdout) { + const checkingResult: ExecutionResult = await batchExecuteWithProgress(cmd, options, 'Checking Lean setup...', folderPath, this.outputChannel) + if (checkingResult.exitCode === ExecutionExitCode.CannotLaunch) { result.error = 'lean not found' - } - else if (stdout.indexOf('no default toolchain') > 0) { + } else if (checkingResult.stderr.indexOf('no default toolchain') > 0) { result.error = 'no default toolchain' - } - else { + } else { const filterVersion = /version (\d+)\.\d+\..+/ - const match = filterVersion.exec(stdout) + const match = filterVersion.exec(checkingResult.stdout) if (!match) { - return { version: '', error: `lean4: '${cmd} ${options}' returned incorrect version string '${stdout}'.` } + return { version: '', error: `lean4: '${cmd} ${options}' returned incorrect version string '${checkingResult.stdout}'.` } } const major = match[1]; result.version = major @@ -250,47 +248,6 @@ export class LeanInstaller { return result } - private async executeWithProgress(prompt: string, cmd: string, options: string[], workingDirectory: string | null): Promise{ - let inc = 0; - let stdout = '' - /* eslint-disable @typescript-eslint/no-this-alias */ - const realThis = this; - await window.withProgress({ - location: ProgressLocation.Notification, - title: '', - cancellable: false - }, (progress) => { - const progressChannel : OutputChannel = { - name : 'ProgressChannel', - append(value: string) - { - stdout += value; - if (realThis.outputChannel){ - // add the output here in case user wants to go look for it. - const msg = value.trim(); - logger.log(`[LeanInstaller] ${cmd} returned: ${msg}`); - realThis.outputChannel.appendLine(msg); - } - if (inc < 100) { - inc += 10; - } - progress.report({ increment: inc, message: value }); - }, - appendLine(value: string) { - this.append(value + '\n'); - }, - replace(value: string) { /* empty */ }, - clear() { /* empty */ }, - show() { /* empty */ }, - hide() { /* empty */ }, - dispose() { /* empty */ } - } - progress.report({increment:0, message: prompt}); - return batchExecute(cmd, options, workingDirectory, progressChannel); - }); - return stdout; - } - getDefaultToolchain() : string { return this.defaultToolchain; } @@ -322,7 +279,7 @@ export class LeanInstaller { try { const cmd = 'elan'; const options = ['toolchain', 'list']; - const stdout = await batchExecute(cmd, options, folderPath, undefined); + const stdout = (await batchExecute(cmd, options, folderPath, undefined)).stdout if (!stdout){ throw new Error('elan toolchain list returned no output.'); } @@ -340,20 +297,15 @@ export class LeanInstaller { } async hasElan() : Promise { - let elanInstalled = false; - // See if we have elan already. try { const options = ['--version'] - const stdout = await this.executeWithProgress('Checking Elan setup...', 'elan', options, null) + const result = await batchExecuteWithProgress('elan', options, 'Checking Elan setup...') const filterVersion = /elan (\d+)\.\d+\..+/ - const match = filterVersion.exec(stdout) - if (match) { - elanInstalled = true; - } + const match = filterVersion.exec(result.stdout) + return match !== null } catch (err) { - elanInstalled = false; + return false } - return elanInstalled; } async installElan() : Promise { diff --git a/vscode-lean4/src/utils/projectInfo.ts b/vscode-lean4/src/utils/projectInfo.ts index 236c0c36d..272a1ad6d 100644 --- a/vscode-lean4/src/utils/projectInfo.ts +++ b/vscode-lean4/src/utils/projectInfo.ts @@ -8,10 +8,16 @@ import path = require('path'); // Detect lean4 root directory (works for both lean4 repo and nightly distribution) export async function isCoreLean4Directory(path: Uri): Promise { - if (path.scheme === 'file'){ - return await fileExists(Uri.joinPath(path, 'LICENSE').fsPath) && await fileExists(Uri.joinPath(path, 'LICENSES').fsPath); + if (path.scheme !== 'file') { + return false } - return false; + + const licensePath = Uri.joinPath(path, 'LICENSE').fsPath + const licensesPath = Uri.joinPath(path, 'LICENSES').fsPath + const srcPath = Uri.joinPath(path, 'src').fsPath + return await fileExists(licensePath) + && await fileExists(licensesPath) + && await fileExists(srcPath) } // Find the root of a Lean project and return an optional WorkspaceFolder for it, @@ -149,15 +155,9 @@ async function readLeanVersionFile(packageFileUri : Uri) : Promise { export async function isValidLeanProject(projectFolder: Uri): Promise { try { const leanToolchainPath = Uri.joinPath(projectFolder, 'lean-toolchain').fsPath - const licensePath = Uri.joinPath(projectFolder, 'LICENSE').fsPath - const licensesPath = Uri.joinPath(projectFolder, 'LICENSES').fsPath - const srcPath = Uri.joinPath(projectFolder, 'src').fsPath const isLeanProject: boolean = await fileExists(leanToolchainPath) - const isLeanItself: boolean = - await fileExists(licensePath) && - await fileExists(licensesPath) && - await fileExists(srcPath) + const isLeanItself: boolean = await isCoreLean4Directory(projectFolder) return isLeanProject || isLeanItself } catch { return false From 8e53afd3d25cf6c747009b3949f5893f8cea5e5f Mon Sep 17 00:00:00 2001 From: mhuisi Date: Fri, 6 Oct 2023 09:48:52 +0200 Subject: [PATCH 07/18] output button in progress, init fix, race condition fix, cancellation --- vscode-lean4/package.json | 15 +- vscode-lean4/src/exports.ts | 6 +- vscode-lean4/src/extension.ts | 43 +- vscode-lean4/src/leanclient.ts | 381 ++++++++++-------- .../src/{project.ts => projectinit.ts} | 154 ++----- vscode-lean4/src/projectoperations.ts | 144 +++++++ vscode-lean4/src/utils/batch.ts | 53 ++- vscode-lean4/src/utils/clientProvider.ts | 4 +- vscode-lean4/src/utils/elan.ts | 7 +- vscode-lean4/src/utils/lake.ts | 33 +- vscode-lean4/src/utils/leanInstaller.ts | 7 +- vscode-lean4/src/utils/manifest.ts | 0 12 files changed, 501 insertions(+), 346 deletions(-) rename vscode-lean4/src/{project.ts => projectinit.ts} (56%) create mode 100644 vscode-lean4/src/projectoperations.ts create mode 100644 vscode-lean4/src/utils/manifest.ts diff --git a/vscode-lean4/package.json b/vscode-lean4/package.json index 754f47ccc..baf6d6027 100644 --- a/vscode-lean4/package.json +++ b/vscode-lean4/package.json @@ -292,6 +292,12 @@ "title": "Docview: Forward", "description": "Go to next page in documentation view" }, + { + "command": "lean4.showOutput", + "category": "Lean 4", + "title": "Show Output", + "description": "Show output channel containing all progress updates and errors of commands" + }, { "command": "lean4.setup.showSetupGuide", "category": "Lean 4", @@ -488,6 +494,9 @@ { "command": "lean4.docView.forward" }, + { + "command": "lean4.showOutput" + }, { "command": "lean4.setup.showSetupGuide" }, @@ -597,17 +606,17 @@ { "command": "lean4.project.build", "when": "editorLangId == lean4", - "group": "4_project@1" + "group": "1_projectActions@1" }, { "command": "lean4.project.clean", "when": "editorLangId == lean4", - "group": "4_project@2" + "group": "1_projectActions@2" }, { "command": "lean4.project.fetchCache", "when": "editorLangId == lean4", - "group": "5_mathlib@1" + "group": "2_mathlibActions@1" } ], "lean4.titlebar.documentation": [ diff --git a/vscode-lean4/src/exports.ts b/vscode-lean4/src/exports.ts index a094bf42d..807520c2f 100644 --- a/vscode-lean4/src/exports.ts +++ b/vscode-lean4/src/exports.ts @@ -2,14 +2,16 @@ import { InfoProvider } from './infoview' import { DocViewProvider } from './docview'; import { LeanInstaller } from './utils/leanInstaller' import { LeanClientProvider } from './utils/clientProvider'; -import { ProjectOperationProvider } from './project'; +import { ProjectInitializationProvider } from './projectinit'; +import { ProjectOperationProvider } from './projectoperations'; export interface Exports { isLean4Project: boolean version: string | undefined infoProvider: InfoProvider | undefined clientProvider: LeanClientProvider | undefined + projectOperationProvider: ProjectOperationProvider | undefined installer: LeanInstaller | undefined docView: DocViewProvider | undefined - projectOperationProvider: ProjectOperationProvider | undefined + projectInitializationProver: ProjectInitializationProvider | undefined } diff --git a/vscode-lean4/src/extension.ts b/vscode-lean4/src/extension.ts index ff895019d..1aa930915 100644 --- a/vscode-lean4/src/extension.ts +++ b/vscode-lean4/src/extension.ts @@ -10,17 +10,19 @@ import { addDefaultElanPath, removeElanPath, addToolchainBinPath, isElanDisabled import { findLeanPackageVersionInfo } from './utils/projectInfo' import { Exports } from './exports'; import { logger } from './utils/logger' -import { ProjectOperationProvider } from './project' +import { ProjectInitializationProvider } from './projectinit' +import { ProjectOperationProvider } from './projectoperations' interface AlwaysEnabledFeatures { docView: DocViewProvider - projectOperationProvider: ProjectOperationProvider + projectInitializationProvider: ProjectInitializationProvider installer: LeanInstaller } interface Lean4EnabledFeatures { clientProvider: LeanClientProvider infoProvider: InfoProvider + projectOperationProvider: ProjectOperationProvider } function isLean(languageId : string) : boolean { @@ -28,8 +30,9 @@ function isLean(languageId : string) : boolean { } function findOpenLeanDocument() : TextDocument | undefined { - if (window.activeTextEditor && isLean(window.activeTextEditor.document.languageId)) { - return window.activeTextEditor.document + const activeEditor = window.activeTextEditor + if (activeEditor && isLean(activeEditor.document.languageId)) { + return activeEditor.document } // This happens if vscode starts with a lean file open @@ -72,14 +75,16 @@ function activateAlwaysEnabledFeatures(context: ExtensionContext): AlwaysEnabled const docView = new DocViewProvider(context.extensionUri); context.subscriptions.push(docView); - const projectOperationProvider = new ProjectOperationProvider() - context.subscriptions.push(projectOperationProvider) - const outputChannel = window.createOutputChannel('Lean: Editor'); + context.subscriptions.push(commands.registerCommand('lean4.showOutput', () => outputChannel.show(true))) + + const projectInitializationProvider = new ProjectInitializationProvider(outputChannel) + context.subscriptions.push(projectInitializationProvider) + const defaultToolchain = getDefaultLeanVersion(); const installer = new LeanInstaller(outputChannel, defaultToolchain) - return { docView, projectOperationProvider, installer } + return { docView, projectInitializationProvider, installer } } async function isLean3Project(installer: LeanInstaller): Promise { @@ -110,14 +115,13 @@ function activateAbbreviationFeature(context: ExtensionContext, docView: DocView return abbrev } -function activateLean4Features(context: ExtensionContext, installer: LeanInstaller, projectOperationProvider: ProjectOperationProvider): Lean4EnabledFeatures { +function activateLean4Features(context: ExtensionContext, installer: LeanInstaller): Lean4EnabledFeatures { const pkgService = new LeanpkgService() pkgService.versionChanged((uri) => installer.handleVersionChanged(uri)); pkgService.lakeFileChanged((uri) => installer.handleLakeFileChanged(uri)); context.subscriptions.push(pkgService); const clientProvider = new LeanClientProvider(installer, pkgService, installer.getOutputChannel()); - projectOperationProvider.clientProvider = clientProvider context.subscriptions.push(clientProvider) const infoProvider = new InfoProvider(clientProvider, {language: 'lean4'}, context); @@ -125,7 +129,9 @@ function activateLean4Features(context: ExtensionContext, installer: LeanInstall context.subscriptions.push(new LeanTaskGutter(clientProvider, context)) - return { clientProvider, infoProvider } + const projectOperationProvider: ProjectOperationProvider = new ProjectOperationProvider(installer.getOutputChannel(), clientProvider) + + return { clientProvider, infoProvider, projectOperationProvider } } export async function activate(context: ExtensionContext): Promise { @@ -137,33 +143,35 @@ export async function activate(context: ExtensionContext): Promise { version: '3', infoProvider: undefined, clientProvider: undefined, + projectOperationProvider: undefined, installer: alwaysEnabledFeatures.installer, docView: alwaysEnabledFeatures.docView, - projectOperationProvider: alwaysEnabledFeatures.projectOperationProvider + projectInitializationProver: alwaysEnabledFeatures.projectInitializationProvider } } activateAbbreviationFeature(context, alwaysEnabledFeatures.docView) if (findOpenLeanDocument()) { - const lean4EnabledFeatures: Lean4EnabledFeatures = activateLean4Features(context, alwaysEnabledFeatures.installer, alwaysEnabledFeatures.projectOperationProvider) + const lean4EnabledFeatures: Lean4EnabledFeatures = activateLean4Features(context, alwaysEnabledFeatures.installer) return { isLean4Project: true, version: '4', infoProvider: lean4EnabledFeatures.infoProvider, clientProvider: lean4EnabledFeatures.clientProvider, + projectOperationProvider: lean4EnabledFeatures.projectOperationProvider, installer: alwaysEnabledFeatures.installer, docView: alwaysEnabledFeatures.docView, - projectOperationProvider: alwaysEnabledFeatures.projectOperationProvider + projectInitializationProver: alwaysEnabledFeatures.projectInitializationProvider } } // No Lean 4 document yet => Load remaining features when one is open const disposeActivationListener: Disposable = window.onDidChangeVisibleTextEditors(_ => { if (findOpenLeanDocument()) { - activateLean4Features(context, alwaysEnabledFeatures.installer, alwaysEnabledFeatures.projectOperationProvider) + activateLean4Features(context, alwaysEnabledFeatures.installer) + disposeActivationListener.dispose() } - disposeActivationListener.dispose() }, context.subscriptions) return { @@ -171,8 +179,9 @@ export async function activate(context: ExtensionContext): Promise { version: undefined, infoProvider: undefined, clientProvider: undefined, + projectOperationProvider: undefined, installer: alwaysEnabledFeatures.installer, docView: alwaysEnabledFeatures.docView, - projectOperationProvider: alwaysEnabledFeatures.projectOperationProvider + projectInitializationProver: alwaysEnabledFeatures.projectInitializationProvider } } diff --git a/vscode-lean4/src/leanclient.ts b/vscode-lean4/src/leanclient.ts index 19200b676..9f3032263 100644 --- a/vscode-lean4/src/leanclient.ts +++ b/vscode-lean4/src/leanclient.ts @@ -30,6 +30,7 @@ import { logger } from './utils/logger' import { SemVer } from 'semver'; import { fileExists, isFileInFolder } from './utils/fsHelper'; import { c2pConverter, p2cConverter, patchConverters } from './utils/converters' +import { Server } from 'http' const escapeRegExp = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); @@ -50,6 +51,7 @@ export class LeanClient implements Disposable { private noPrompt : boolean = false; private showingRestartMessage : boolean = false; private elanDefaultToolchain: string; + private isRestarting: boolean = false private didChangeEmitter = new EventEmitter() didChange = this.didChangeEmitter.event @@ -131,6 +133,11 @@ export class LeanClient implements Disposable { } async restart(): Promise { + if (this.isRestarting) { + await window.showErrorMessage('Client is already being started.') + return + } + this.isRestarting = true const startTime = Date.now() logger.log('[LeanClient] Restarting Lean Server') @@ -140,178 +147,12 @@ export class LeanClient implements Disposable { this.restartingEmitter.fire(undefined) this.toolchainPath = toolchainPath(); - let version: string | null = null; - const env = addServerEnvPaths(process.env); - if (serverLoggingEnabled()) { - env.LEAN_SERVER_LOG_DIR = serverLoggingPath() - } - let executable = lakePath() || - (this.toolchainPath ? join(this.toolchainPath, 'bin', 'lake') : 'lake'); + this.client = await this.setupClient() - // check if the lake process will start (skip it on scheme: 'untitled' files) - let useLake = lakeEnabled() && this.folderUri && this.folderUri.scheme === 'file'; - if (useLake) { - let knownDate = false; - const lakefile = Uri.joinPath(this.folderUri, 'lakefile.lean') - if (!await fileExists(new URL(lakefile.toString()))) { - useLake = false; - } - else { - // see if we can avoid the more expensive checkLakeVersion call. - const date = await this.checkToolchainVersion(this.folderUri); - if (date){ - // Feb 16 2022 is when the 3.1.0.pre was released. - useLake = date >= new Date(2022, 1, 16); - knownDate = true; - } - if (useLake && !knownDate){ - useLake = await this.checkLakeVersion(executable, version); - } - } - } - - if (!useLake) { - executable = (this.toolchainPath) ? join(this.toolchainPath, 'bin', 'lean') : 'lean'; - } - - const cwd = this.folderUri?.fsPath - if (!cwd && !version){ - // Fixes issue #227, for adhoc files it would pick up the cwd from the open folder - // which is not what we want. For adhoc files we want the (default) toolchain instead. - version = this.elanDefaultToolchain; - } - - let options = version ? ['+' + version] :[] - if (useLake) { - options = options.concat(['serve', '--']) - } else{ - options = options.concat(['--server']) - } - - // Add folder name to command-line so that it shows up in `ps aux`. - if (cwd) { - options.push('' + cwd) - } else { - options.push('untitled') - } - - const serverOptions: ServerOptions = { - command: executable, - args: options.concat(serverArgs()), - options: { - cwd, - env - } - } - - const documentSelector: DocumentFilter = { - language: 'lean4' - } - - if (this.folderUri){ - documentSelector.scheme = this.folderUri.scheme - if (this.folderUri.scheme !== 'untitled') { - documentSelector.pattern = `${this.folderUri.fsPath}/**/*` - } - } - - const clientOptions: LanguageClientOptions = { - outputChannel: this.outputChannel, - revealOutputChannelOn: RevealOutputChannelOn.Never, // contrary to the name, this disables the message boxes - documentSelector: [documentSelector], - workspaceFolder: this.workspaceFolder, - initializationOptions: { - editDelay: getElaborationDelay(), hasWidgets: true, - }, - connectionOptions: { - maxRestartCount: 0, - cancellationStrategy: undefined as any, - }, - middleware: { - handleDiagnostics: (uri, diagnostics, next) => { - next(uri, diagnostics); - if (!this.client) return; - const uri_ = c2pConverter.asUri(uri); - const diagnostics_ = []; - for (const d of diagnostics) { - const d_: ls.Diagnostic = { - ...c2pConverter.asDiagnostic(d), - }; - diagnostics_.push(d_); - } - this.diagnosticsEmitter.fire({uri: uri_, diagnostics: diagnostics_}); - }, - - didOpen: async () => { - // Note: as per the LSP spec: An open notification must not be sent more than once - // without a corresponding close notification send before. This means open and close - // notification must be balanced and the max open count for a particular textDocument - // is one. So this even does nothing the notification is handled by the - // openLean4Document method below after the 'lean4' languageId is established and - // it has weeded out documents opened to invisible editors (like 'git:' schemes and - // invisible editors created for Ctrl+Hover events. A side effect of unbalanced - // open/close notification is leaking 'lean --worker' processes. - // See https://github.com/microsoft/vscode/issues/78453). - return; - }, - - didChange: async (data, next) => { - await next(data); - if (!this.running || !this.client) return; // there was a problem starting lean server. - const params = c2pConverter.asChangeTextDocumentParams(data); - this.didChangeEmitter.fire(params); - }, - - didClose: async (doc, next) => { - if (!this.isOpen.delete(doc.uri.toString())) { - return; - } - await next(doc); - if (!this.running || !this.client) return; // there was a problem starting lean server. - const params = c2pConverter.asCloseTextDocumentParams(doc); - this.didCloseEmitter.fire(params); - }, - - provideDocumentHighlights: async (doc, pos, ctok, next) => { - const leanHighlights = await next(doc, pos, ctok); - if (leanHighlights?.length) return leanHighlights; - - // vscode doesn't fall back to textual highlights, - // so we need to do that manually - await new Promise((res) => setTimeout(res, 250)); - if (ctok.isCancellationRequested) return; - - const wordRange = doc.getWordRangeAtPosition(pos); - if (!wordRange) return; - const word = doc.getText(wordRange); - - const highlights: DocumentHighlight[] = []; - const text = doc.getText(); - const nonWordPattern = '[`~@$%^&*()-=+\\[{\\]}⟨⟩⦃⦄⟦⟧⟮⟯‹›\\\\|;:\",./\\s]|^|$' - const regexp = new RegExp(`(?<=${nonWordPattern})${escapeRegExp(word)}(?=${nonWordPattern})`, 'g') - for (const match of text.matchAll(regexp)) { - const start = doc.positionAt(match.index ?? 0) - highlights.push({ - range: new Range(start, start.translate(0, match[0].length)), - kind: DocumentHighlightKind.Text, - }) - } - - return highlights; - } - }, - } - this.client = new LanguageClient( - 'lean4', - 'Lean 4', - serverOptions, - clientOptions - ) let insideRestart = true; - patchConverters(this.client.protocol2CodeConverter, this.client.code2ProtocolConverter) try { - this.client.onDidChangeState(async (s) => { + this.client.onDidChangeState(async s => { // see https://github.com/microsoft/vscode-languageserver-node/issues/825 if (s.newState === State.Starting) { logger.log('[LeanClient] starting'); @@ -325,7 +166,7 @@ export class LeanClient implements Disposable { } else if (s.newState === State.Stopped) { this.running = false; logger.log('[LeanClient] has stopped or it failed to start'); - if (!this.noPrompt){ + if (!this.noPrompt) { // only raise this event and show the message if we are not the ones // who called the stop() method. this.stoppedEmitter.fire({message:'Lean server has stopped.', reason:''}); @@ -347,6 +188,7 @@ export class LeanClient implements Disposable { this.outputChannel.appendLine(msg); this.serverFailedEmitter.fire(msg); insideRestart = false; + this.isRestarting = false return; } @@ -372,7 +214,7 @@ export class LeanClient implements Disposable { // Reveal the standard error output channel when the server prints something to stderr. // The vscode-languageclient library already takes care of writing it to the output channel. let stderrMsgBoxVisible = false; - (this.client as any)._serverProcess.stderr.on('data', async (chunk : Buffer) => { + (this.client as any)._serverProcess.stderr.on('data', async (chunk: Buffer) => { if (shouldAutofocusOutput()) { this.client?.outputChannel.show(true); } else if (!stderrMsgBoxVisible) { @@ -388,6 +230,25 @@ export class LeanClient implements Disposable { this.restartedEmitter.fire(undefined) insideRestart = false; + this.isRestarting = false + } + + async withStoppedClient(action: () => Promise): Promise<'Success' | 'IsRestarting'> { + if (this.isRestarting) { + return 'IsRestarting' + } + this.isRestarting = true // Ensure that client cannot be restarted in the mean-time + + if (this.isStarted()) { + await this.stop() + } + + await action() + + this.isRestarting = false + await this.restart() + + return 'Success' } async openLean4Document(doc: TextDocument) { @@ -562,7 +423,7 @@ export class LeanClient implements Disposable { // Check that the Lake version is high enough to support "lake serve" option. const versionOptions = version ? ['+' + version, '--version'] : ['--version'] const start = Date.now() - const result: ExecutionResult = await batchExecute(executable, versionOptions, this.folderUri?.fsPath, undefined); + const result: ExecutionResult = await batchExecute(executable, versionOptions, this.folderUri?.fsPath); if (result.exitCode !== ExecutionExitCode.Success) { return false } @@ -587,4 +448,182 @@ export class LeanClient implements Disposable { return new SemVer('0.0.0'); } } + + private async determineServerOptions(): Promise { + const env = addServerEnvPaths(process.env) + if (serverLoggingEnabled()) { + env.LEAN_SERVER_LOG_DIR = serverLoggingPath() + } + + const [serverExecutable, options] = await this.determineExecutable() + + const cwd = this.folderUri?.fsPath + if (cwd) { + // Add folder name to command-line so that it shows up in `ps aux`. + options.push(cwd) + } else { + // Fixes issue #227, for adhoc files it would pick up the cwd from the open folder + // which is not what we want. For adhoc files we want the (default) toolchain instead. + options.unshift('+' + this.elanDefaultToolchain) + options.push('untitled') + } + + return { + command: serverExecutable, + args: options.concat(serverArgs()), + options: { + cwd, + env + } + } + } + + private async determineExecutable(): Promise<[string, string[]]> { + const lakeExecutable = lakePath() || + (this.toolchainPath ? join(this.toolchainPath, 'bin', 'lake') : 'lake') + const leanExecutable = + (this.toolchainPath) ? join(this.toolchainPath, 'bin', 'lean') : 'lean' + + if (await this.shouldUseLake(lakeExecutable)) { + return [lakeExecutable, ['serve', '--']] + } else{ + return [leanExecutable, ['--server']] + } + } + + private async shouldUseLake(lakeExecutable: string): Promise { + // check if the lake process will start (skip it on scheme: 'untitled' files) + if (!lakeEnabled() || !this.folderUri || this.folderUri.scheme !== 'file') { + return false + } + + const lakefile = Uri.joinPath(this.folderUri, 'lakefile.lean') + if (!await fileExists(new URL(lakefile.toString()))) { + return false + } + + // see if we can avoid the more expensive checkLakeVersion call. + const date = await this.checkToolchainVersion(this.folderUri); + if (date) { + // Feb 16 2022 is when the 3.1.0.pre was released. + return date >= new Date(2022, 1, 16); + } + + return await this.checkLakeVersion(lakeExecutable, null); + } + + private obtainClientOptions(): LanguageClientOptions { + const documentSelector: DocumentFilter = { + language: 'lean4' + } + + if (this.folderUri){ + documentSelector.scheme = this.folderUri.scheme + if (this.folderUri.scheme !== 'untitled') { + documentSelector.pattern = `${this.folderUri.fsPath}/**/*` + } + } + + return { + outputChannel: this.outputChannel, + revealOutputChannelOn: RevealOutputChannelOn.Never, // contrary to the name, this disables the message boxes + documentSelector: [documentSelector], + workspaceFolder: this.workspaceFolder, + initializationOptions: { + editDelay: getElaborationDelay(), hasWidgets: true, + }, + connectionOptions: { + maxRestartCount: 0, + cancellationStrategy: undefined as any, + }, + middleware: { + handleDiagnostics: (uri, diagnostics, next) => { + next(uri, diagnostics); + if (!this.client) return; + const uri_ = c2pConverter.asUri(uri); + const diagnostics_ = []; + for (const d of diagnostics) { + const d_: ls.Diagnostic = { + ...c2pConverter.asDiagnostic(d), + }; + diagnostics_.push(d_); + } + this.diagnosticsEmitter.fire({uri: uri_, diagnostics: diagnostics_}); + }, + + didOpen: async () => { + // Note: as per the LSP spec: An open notification must not be sent more than once + // without a corresponding close notification send before. This means open and close + // notification must be balanced and the max open count for a particular textDocument + // is one. So this even does nothing the notification is handled by the + // openLean4Document method below after the 'lean4' languageId is established and + // it has weeded out documents opened to invisible editors (like 'git:' schemes and + // invisible editors created for Ctrl+Hover events. A side effect of unbalanced + // open/close notification is leaking 'lean --worker' processes. + // See https://github.com/microsoft/vscode/issues/78453). + return; + }, + + didChange: async (data, next) => { + await next(data); + if (!this.running || !this.client) return; // there was a problem starting lean server. + const params = c2pConverter.asChangeTextDocumentParams(data); + this.didChangeEmitter.fire(params); + }, + + didClose: async (doc, next) => { + if (!this.isOpen.delete(doc.uri.toString())) { + return; + } + await next(doc); + if (!this.running || !this.client) return; // there was a problem starting lean server. + const params = c2pConverter.asCloseTextDocumentParams(doc); + this.didCloseEmitter.fire(params); + }, + + provideDocumentHighlights: async (doc, pos, ctok, next) => { + const leanHighlights = await next(doc, pos, ctok); + if (leanHighlights?.length) return leanHighlights; + + // vscode doesn't fall back to textual highlights, + // so we need to do that manually + await new Promise((res) => setTimeout(res, 250)); + if (ctok.isCancellationRequested) return; + + const wordRange = doc.getWordRangeAtPosition(pos); + if (!wordRange) return; + const word = doc.getText(wordRange); + + const highlights: DocumentHighlight[] = []; + const text = doc.getText(); + const nonWordPattern = '[`~@$%^&*()-=+\\[{\\]}⟨⟩⦃⦄⟦⟧⟮⟯‹›\\\\|;:\",./\\s]|^|$' + const regexp = new RegExp(`(?<=${nonWordPattern})${escapeRegExp(word)}(?=${nonWordPattern})`, 'g') + for (const match of text.matchAll(regexp)) { + const start = doc.positionAt(match.index ?? 0) + highlights.push({ + range: new Range(start, start.translate(0, match[0].length)), + kind: DocumentHighlightKind.Text, + }) + } + + return highlights; + } + }, + } + } + + private async setupClient(): Promise { + const serverOptions: ServerOptions = await this.determineServerOptions() + const clientOptions: LanguageClientOptions = this.obtainClientOptions() + + const client = new LanguageClient( + 'lean4', + 'Lean 4', + serverOptions, + clientOptions + ) + + patchConverters(client.protocol2CodeConverter, client.code2ProtocolConverter) + return client + } } diff --git a/vscode-lean4/src/project.ts b/vscode-lean4/src/projectinit.ts similarity index 56% rename from vscode-lean4/src/project.ts rename to vscode-lean4/src/projectinit.ts index 837ecccb1..8ffdd7b29 100644 --- a/vscode-lean4/src/project.ts +++ b/vscode-lean4/src/projectinit.ts @@ -1,27 +1,21 @@ -import { Disposable, Uri, commands, window, workspace, SaveDialogOptions } from 'vscode'; +import { Disposable, Uri, commands, window, workspace, SaveDialogOptions, OutputChannel } from 'vscode'; import path = require('path'); import { checkParentFoldersForLeanProject, isValidLeanProject } from './utils/projectInfo'; import { elanSelfUpdate } from './utils/elan'; -import { LakeRunner, cacheNotFoundError, lake, lakeInActiveFolder } from './utils/lake'; -import { ExecutionExitCode, ExecutionResult, batchExecute, batchExecuteWithProgress, displayError } from './utils/batch'; -import { LeanClientProvider } from './utils/clientProvider'; -import { LeanClient } from './leanclient'; +import { lake } from './utils/lake'; +import { ExecutionExitCode, ExecutionResult, batchExecuteWithProgress, displayError } from './utils/batch'; -export class ProjectOperationProvider implements Disposable { +export class ProjectInitializationProvider implements Disposable { private subscriptions: Disposable[] = []; - clientProvider: LeanClientProvider | undefined = undefined // set when the lean 4 client loads - constructor() { + constructor(private channel: OutputChannel) { this.subscriptions.push( commands.registerCommand('lean4.project.createLibraryProject', () => this.createLibraryProject()), commands.registerCommand('lean4.project.createProgramProject', () => this.createProgramProject()), commands.registerCommand('lean4.project.createMathlibProject', () => this.createMathlibProject()), commands.registerCommand('lean4.project.open', () => this.openProject()), - commands.registerCommand('lean4.project.clone', () => this.cloneProject()), - commands.registerCommand('lean4.project.build', () => this.buildProject()), - commands.registerCommand('lean4.project.clean', () => this.cleanProject()), - commands.registerCommand('lean4.project.fetchCache', () => this.fetchMathlibCache()) + commands.registerCommand('lean4.project.clone', () => this.cloneProject()) ) } @@ -29,7 +23,7 @@ export class ProjectOperationProvider implements Disposable { const projectFolder: Uri | 'DidNotComplete' = await this.createProject('lib', 'library', 'stable') if (projectFolder !== 'DidNotComplete') { - await ProjectOperationProvider.openNewFolder(projectFolder) + await ProjectInitializationProvider.openNewFolder(projectFolder) } } @@ -37,7 +31,7 @@ export class ProjectOperationProvider implements Disposable { const projectFolder: Uri | 'DidNotComplete' = await this.createProject('exe', 'program', 'stable') if (projectFolder !== 'DidNotComplete') { - await ProjectOperationProvider.openNewFolder(projectFolder) + await ProjectInitializationProvider.openNewFolder(projectFolder) } } @@ -49,19 +43,25 @@ export class ProjectOperationProvider implements Disposable { return } - const updateResult: ExecutionResult = await lake(projectFolder, mathlibToolchain).updateDependencies() + const updateResult: ExecutionResult = await lake(this.channel, projectFolder, mathlibToolchain).updateDependencies() + if (updateResult.exitCode === ExecutionExitCode.Cancelled) { + return + } if (updateResult.exitCode !== ExecutionExitCode.Success) { await displayError(updateResult, 'Cannot update dependencies.') return } - const cacheGetResult: ExecutionResult = await lake(projectFolder, mathlibToolchain).fetchMathlibCache() + const cacheGetResult: ExecutionResult = await lake(this.channel, projectFolder, mathlibToolchain).fetchMathlibCache() + if (cacheGetResult.exitCode === ExecutionExitCode.Cancelled) { + return + } if (cacheGetResult.exitCode !== ExecutionExitCode.Success) { await displayError(cacheGetResult, 'Cannot fetch Mathlib build artifact cache.') return } - await ProjectOperationProvider.openNewFolder(projectFolder) + await ProjectInitializationProvider.openNewFolder(projectFolder) } private async createProject( @@ -69,7 +69,7 @@ export class ProjectOperationProvider implements Disposable { kindName: string, toolchain?: string | undefined): Promise { - const projectFolder: Uri | undefined = await ProjectOperationProvider.askForNewProjectFolderLocation({ + const projectFolder: Uri | undefined = await ProjectInitializationProvider.askForNewProjectFolderLocation({ saveLabel: 'Create project folder', title: `Create a new ${kindName} project folder` }) @@ -79,10 +79,11 @@ export class ProjectOperationProvider implements Disposable { await workspace.fs.createDirectory(projectFolder) - await elanSelfUpdate() + // This can fail silently in setups without Elan. + await elanSelfUpdate(this.channel) const projectName: string = path.basename(projectFolder.fsPath) - const result: ExecutionResult = await lake(projectFolder, toolchain).initProject(projectName, kind) + const result: ExecutionResult = await lake(this.channel, projectFolder, toolchain).initProject(projectName, kind) if (result.exitCode !== ExecutionExitCode.Success) { await displayError(result, 'Cannot initialize project.') return 'DidNotComplete' @@ -104,12 +105,12 @@ export class ProjectOperationProvider implements Disposable { } let projectFolder = projectFolders[0] - if (!await ProjectOperationProvider.checkIsFileUriOrShowError(projectFolder)) { + if (!await ProjectInitializationProvider.checkIsFileUriOrShowError(projectFolder)) { return } if (!await isValidLeanProject(projectFolder)) { - const parentProjectFolder: Uri | undefined = await ProjectOperationProvider.attemptFindingLeanProjectInParentFolder(projectFolder) + const parentProjectFolder: Uri | undefined = await ProjectInitializationProvider.attemptFindingLeanProjectInParentFolder(projectFolder) if (parentProjectFolder === undefined) { return } @@ -158,7 +159,7 @@ Open this project instead?` } const existingProjectUri = Uri.parse(unparsedProjectUri) - const projectFolder: Uri | undefined = await ProjectOperationProvider.askForNewProjectFolderLocation({ + const projectFolder: Uri | undefined = await ProjectInitializationProvider.askForNewProjectFolderLocation({ saveLabel: 'Create project folder', title: 'Create a new project folder to clone existing Lean 4 project into' }) @@ -166,16 +167,22 @@ Open this project instead?` return } - const result: ExecutionResult = await batchExecuteWithProgress('git', ['clone', existingProjectUri.toString(), projectFolder.fsPath], 'Cloning project ...') + const result: ExecutionResult = await batchExecuteWithProgress('git', ['clone', existingProjectUri.toString(), projectFolder.fsPath], 'Cloning project ...', { channel: this.channel, allowCancellation: true }) + if (result.exitCode === ExecutionExitCode.Cancelled) { + return + } if (result.exitCode !== ExecutionExitCode.Success) { await displayError(result, 'Cannot download project.') return } // Try it. If this is not a mathlib project, it will fail silently. Otherwise, it will grab the cache. - await lake(projectFolder).fetchMathlibCache(true) + const fetchResult: ExecutionResult = await lake(this.channel, projectFolder).fetchMathlibCache(true) + if (fetchResult.exitCode === ExecutionExitCode.Cancelled) { + return + } - await ProjectOperationProvider.openNewFolder(projectFolder) + await ProjectInitializationProvider.openNewFolder(projectFolder) } private static async askForNewProjectFolderLocation(options: SaveDialogOptions): Promise { @@ -205,101 +212,6 @@ Open this project instead?` } } - private async buildProject() { - await this.inActiveFolderWithoutServer(async lakeRunner => { - // Try it. If this is not a mathlib project, it will fail silently. Otherwise, it will grab the cache. - await lakeRunner.fetchMathlibCache(true) - - const result: ExecutionResult = await lakeRunner.build() - if (result.exitCode !== ExecutionExitCode.Success) { - void displayError(result, 'Cannot build project.') - return - } - - void window.showInformationMessage('Project built successfully.') - return - }) - } - - private async cleanProject() { - const deleteInput = 'Proceed' - const deleteChoice: string | undefined = await window.showInformationMessage('Delete all build artifacts?', { modal: true }, deleteInput) - if (deleteChoice !== deleteInput) { - return - } - - await this.inActiveFolderWithoutServer(async lakeRunner => { - const cleanResult: ExecutionResult = await lakeRunner.clean() - if (cleanResult.exitCode !== ExecutionExitCode.Success) { - void displayError(cleanResult, 'Cannot delete build artifacts.') - return - } - - if (!await lakeRunner.isMathlibCacheGetAvailable()) { - void window.showInformationMessage('Project cleaned successfully.') - return - } - - const fetchMessage = 'Project cleaned successfully. Do you want to fetch Mathlib\'s build artifact cache?' - const fetchInput = 'Fetch Cache' - const fetchChoice: string | undefined = await window.showInformationMessage(fetchMessage, { modal: true }, fetchInput) - if (fetchChoice !== fetchInput) { - return - } - - const fetchResult: ExecutionResult = await lakeRunner.fetchMathlibCache() - if (fetchResult.exitCode !== ExecutionExitCode.Success) { - void displayError(fetchResult, 'Cannot fetch Mathlib build artifact cache.') - return - } - void window.showInformationMessage('Mathlib build artifact cache fetched successfully.') - }) - } - - private async fetchMathlibCache() { - await this.inActiveFolderWithoutServer(async lakeRunner => { - const result: ExecutionResult = await lakeRunner.fetchMathlibCache() - if (result.exitCode !== ExecutionExitCode.Success) { - if (result.stderr.includes(cacheNotFoundError)) { - void window.showErrorMessage('This command cannot be used in non-Mathlib projects.') - return - } - void displayError(result, 'Cannot fetch Mathlib build artifact cache.') - return - } - - void window.showInformationMessage('Mathlib build artifact cache fetched successfully.') - }) - } - - private async inActiveFolderWithoutServer(command: (lakeRunner: LakeRunner) => Promise) { - if (!this.clientProvider) { - void window.showErrorMessage('Lean client has not been loaded yet.') - return - } - - const lakeRunner: LakeRunner | 'NoActiveFolder' = await lakeInActiveFolder() - if (lakeRunner === 'NoActiveFolder') { - return - } - - const activeClient: LeanClient | undefined = this.clientProvider.getActiveClient() - if (!activeClient) { - void window.showErrorMessage('No active client.') - return - } - - if (activeClient.isRunning()) { - await activeClient.stop() - } - - await command(lakeRunner) - - if (!activeClient.isRunning()) { - await activeClient.start() - } - } - dispose() { for (const s of this.subscriptions) { s.dispose(); } } diff --git a/vscode-lean4/src/projectoperations.ts b/vscode-lean4/src/projectoperations.ts new file mode 100644 index 000000000..72b7fea86 --- /dev/null +++ b/vscode-lean4/src/projectoperations.ts @@ -0,0 +1,144 @@ +import { Disposable, commands, window, OutputChannel } from 'vscode'; +import { LakeRunner, cacheNotFoundError, lakeInActiveFolder } from './utils/lake'; +import { ExecutionExitCode, ExecutionResult, displayError } from './utils/batch'; +import { LeanClientProvider } from './utils/clientProvider'; +import { LeanClient } from './leanclient'; + +export class ProjectOperationProvider implements Disposable { + + private subscriptions: Disposable[] = [] + private isRunningOperation: boolean = false // Used to synchronize project operations + + constructor(private channel: OutputChannel, private clientProvider: LeanClientProvider) { + this.subscriptions.push( + commands.registerCommand('lean4.project.build', () => this.buildProject()), + commands.registerCommand('lean4.project.clean', () => this.cleanProject()), + commands.registerCommand('lean4.project.fetchCache', () => this.fetchMathlibCache()) + ) + } + + private async buildProject() { + await this.runOperation(async lakeRunner => { + // Try it. If this is not a mathlib project, it will fail silently. Otherwise, it will grab the cache. + const fetchResult: ExecutionResult = await lakeRunner.fetchMathlibCache(true) + if (fetchResult.exitCode === ExecutionExitCode.Cancelled) { + return + } + + const result: ExecutionResult = await lakeRunner.build() + if (result.exitCode === ExecutionExitCode.Cancelled) { + return + } + if (result.exitCode !== ExecutionExitCode.Success) { + void displayError(result, 'Cannot build project.') + return + } + + void window.showInformationMessage('Project built successfully.') + return + }) + } + + private async cleanProject() { + const deleteInput = 'Proceed' + const deleteChoice: string | undefined = await window.showInformationMessage('Delete all build artifacts?', { modal: true }, deleteInput) + if (deleteChoice !== deleteInput) { + return + } + + await this.runOperation(async lakeRunner => { + const cleanResult: ExecutionResult = await lakeRunner.clean() + if (cleanResult.exitCode === ExecutionExitCode.Cancelled) { + return + } + if (cleanResult.exitCode !== ExecutionExitCode.Success) { + void displayError(cleanResult, 'Cannot delete build artifacts.') + return + } + + const checkResult: 'Yes' | 'No' | 'Cancelled' = await lakeRunner.isMathlibCacheGetAvailable() + if (checkResult === 'Cancelled') { + return + } + if (checkResult === 'No') { + void window.showInformationMessage('Project cleaned successfully.') + return + } + + const fetchMessage = 'Project cleaned successfully. Do you want to fetch Mathlib\'s build artifact cache?' + const fetchInput = 'Fetch Cache' + const fetchChoice: string | undefined = await window.showInformationMessage(fetchMessage, { modal: true }, fetchInput) + if (fetchChoice !== fetchInput) { + return + } + + const fetchResult: ExecutionResult = await lakeRunner.fetchMathlibCache() + if (fetchResult.exitCode === ExecutionExitCode.Cancelled) { + return + } + if (fetchResult.exitCode !== ExecutionExitCode.Success) { + void displayError(fetchResult, 'Cannot fetch Mathlib build artifact cache.') + return + } + void window.showInformationMessage('Mathlib build artifact cache fetched successfully.') + }) + } + + private async fetchMathlibCache() { + await this.runOperation(async lakeRunner => { + const result: ExecutionResult = await lakeRunner.fetchMathlibCache() + if (result.exitCode === ExecutionExitCode.Cancelled) { + return + } + if (result.exitCode !== ExecutionExitCode.Success) { + if (result.stderr.includes(cacheNotFoundError)) { + void window.showErrorMessage('This command cannot be used in non-Mathlib projects.') + return + } + void displayError(result, 'Cannot fetch Mathlib build artifact cache.') + return + } + + void window.showInformationMessage('Mathlib build artifact cache fetched successfully.') + }) + } + + private async runOperation(command: (lakeRunner: LakeRunner) => Promise) { + if (this.isRunningOperation) { + void window.showErrorMessage('Another project action is already being executed. Please wait for its completion.') + return + } + this.isRunningOperation = true + + if (!this.clientProvider) { + void window.showErrorMessage('Lean client has not loaded yet.') + this.isRunningOperation = false + return + } + + const lakeRunner: LakeRunner | 'NoActiveFolder' = await lakeInActiveFolder(this.channel) + if (lakeRunner === 'NoActiveFolder') { + this.isRunningOperation = false + return + } + + const activeClient: LeanClient | undefined = this.clientProvider.getActiveClient() + if (!activeClient) { + void window.showErrorMessage('No active client.') + this.isRunningOperation = false + return + } + + const result: 'Success' | 'IsRestarting' = await activeClient.withStoppedClient(() => command(lakeRunner)) + if (result === 'IsRestarting') { + void window.showErrorMessage('Cannot run project action while restarting the server.') + } + + this.isRunningOperation = false + } + + dispose() { + for (const s of this.subscriptions) { s.dispose(); } + } + +} diff --git a/vscode-lean4/src/utils/batch.ts b/vscode-lean4/src/utils/batch.ts index 474c03b19..1eb417b02 100644 --- a/vscode-lean4/src/utils/batch.ts +++ b/vscode-lean4/src/utils/batch.ts @@ -1,4 +1,4 @@ -import { OutputChannel, ProgressLocation, ProgressOptions, window } from 'vscode' +import { CancellationToken, Disposable, OutputChannel, ProgressLocation, ProgressOptions, window } from 'vscode' import { spawn } from 'child_process'; import { findProgramInPath, isRunningTest } from '../config' import { logger } from './logger' @@ -12,7 +12,8 @@ export interface ExecutionChannel { export enum ExecutionExitCode { Success, CannotLaunch, - ExecutionError + ExecutionError, + Cancelled } export interface ExecutionResult { @@ -33,7 +34,8 @@ export async function batchExecute( executablePath: string, args: string[], workingDirectory?: string | undefined, - channel?: ExecutionChannel | undefined): Promise { + channel?: ExecutionChannel | undefined, + token?: CancellationToken | undefined): Promise { return new Promise(function(resolve, reject) { let stdout: string = '' @@ -58,7 +60,10 @@ export async function batchExecute( } const proc = spawn(executablePath, args, options); + const disposeKill: Disposable | undefined = token?.onCancellationRequested(_ => proc.kill()) + proc.on('error', err => { + disposeKill?.dispose() resolve(createCannotLaunchExecutionResult(err.message)) }); @@ -76,8 +81,17 @@ export async function batchExecute( stderr += s + '\n'; }); - proc.on('close', (code) => { + proc.on('close', (code, signal) => { + disposeKill?.dispose() logger.log(`child process exited with code ${code}`); + if (signal === 'SIGTERM') { + resolve({ + exitCode: ExecutionExitCode.Cancelled, + stdout, + stderr + }) + return + } if (code !== 0) { resolve({ exitCode: ExecutionExitCode.ExecutionError, @@ -93,46 +107,53 @@ export async function batchExecute( }) }); - } catch (e){ + } catch (e) { logger.log(`error running ${executablePath} : ${e}`); resolve(createCannotLaunchExecutionResult('')); } }); } +interface ProgressExecutionOptions { + cwd?: string | undefined + channel?: OutputChannel | undefined + translator?: ((line: string) => string | undefined) | undefined + allowCancellation?: boolean +} + export async function batchExecuteWithProgress( executablePath: string, args: string[], prompt: string, - workingDirectory?: string | undefined, - channel?: OutputChannel | undefined, - translator?: ((line: string) => string | undefined) | undefined): Promise { + options: ProgressExecutionOptions = {}): Promise { + + const messagePrefix = options.channel ? '[(Details)](command:lean4.showOutput) ' : '' const progressOptions: ProgressOptions = { location: ProgressLocation.Notification, title: '', - cancellable: false + cancellable: options.allowCancellation === true } let inc = 0 - const result: ExecutionResult = await window.withProgress(progressOptions, progress => { + const result: ExecutionResult = await window.withProgress(progressOptions, (progress, token) => { const progressChannel: OutputChannel = { name : 'ProgressChannel', append(value: string) { - if (translator) { - const translatedValue: string | undefined = translator(value) + if (options.translator) { + const translatedValue: string | undefined = options.translator(value) if (translatedValue === undefined) { return } value = translatedValue } - if (channel) { - channel.appendLine(value) + if (options.channel) { + options.channel.appendLine(value) } if (inc < 90) { inc += 2 } - progress.report({ increment: inc, message: value }) + progress.report({ increment: inc, message: messagePrefix + value }) }, appendLine(value: string) { this.append(value + '\n') @@ -144,7 +165,7 @@ export async function batchExecuteWithProgress( dispose() { /* empty */ } } progress.report({ increment: 0, message: prompt }); - return batchExecute(executablePath, args, workingDirectory, { combined: progressChannel }); + return batchExecute(executablePath, args, options.cwd, { combined: progressChannel }, token); }); return result; } diff --git a/vscode-lean4/src/utils/clientProvider.ts b/vscode-lean4/src/utils/clientProvider.ts index 56bb4eb4a..135bcac99 100644 --- a/vscode-lean4/src/utils/clientProvider.ts +++ b/vscode-lean4/src/utils/clientProvider.ts @@ -145,7 +145,9 @@ export class LeanClientProvider implements Disposable { } private stopActiveClient() { - void this.activeClient?.stop(); + if (this.activeClient && this.activeClient.isStarted()) { + void this.activeClient?.stop(); + } } private async restartActiveClient() { diff --git a/vscode-lean4/src/utils/elan.ts b/vscode-lean4/src/utils/elan.ts index 2e44ee978..e79218692 100644 --- a/vscode-lean4/src/utils/elan.ts +++ b/vscode-lean4/src/utils/elan.ts @@ -1,5 +1,6 @@ -import { ExecutionResult, batchExecute } from './batch'; +import { OutputChannel } from 'vscode'; +import { ExecutionResult, batchExecuteWithProgress } from './batch'; -export async function elanSelfUpdate(): Promise { - return await batchExecute('elan', ['self', 'update']) +export async function elanSelfUpdate(channel: OutputChannel): Promise { + return await batchExecuteWithProgress('elan', ['self', 'update'], 'Updating Elan ...', { channel }) } diff --git a/vscode-lean4/src/utils/lake.ts b/vscode-lean4/src/utils/lake.ts index 999877d7b..b4c9bd6f6 100644 --- a/vscode-lean4/src/utils/lake.ts +++ b/vscode-lean4/src/utils/lake.ts @@ -1,14 +1,16 @@ -import { Uri, window } from 'vscode'; +import { OutputChannel, Uri, window } from 'vscode'; import { ExecutionExitCode, ExecutionResult, batchExecute, batchExecuteWithProgress } from './batch'; import { findLeanPackageRoot } from './projectInfo'; export const cacheNotFoundError = 'unknown executable `cache`' export class LakeRunner { + channel: OutputChannel cwdUri: Uri | undefined toolchain: string | undefined - constructor(cwdUri: Uri | undefined, toolchain?: string | undefined) { + constructor(channel: OutputChannel, cwdUri: Uri | undefined, toolchain?: string | undefined) { + this.channel = channel this.cwdUri = cwdUri this.toolchain = toolchain } @@ -39,9 +41,15 @@ export class LakeRunner { }) } - async isMathlibCacheGetAvailable(): Promise { - const result: ExecutionResult = await this.runLakeCommandSilently('exe', ['cache']) - return result.exitCode === ExecutionExitCode.Success + async isMathlibCacheGetAvailable(): Promise<'Yes' | 'No' | 'Cancelled'> { + const result: ExecutionResult = await this.runLakeCommandWithProgress('exe', ['cache'], 'Checking whether this is a Mathlib project ...') + if (result.exitCode === ExecutionExitCode.Cancelled) { + return 'Cancelled' + } + if (result.exitCode === ExecutionExitCode.Success) { + return 'Yes' + } + return 'No' } private async runLakeCommandSilently(subCommand: string, args: string[]): Promise { @@ -63,15 +71,20 @@ export class LakeRunner { if (this.toolchain) { args.unshift(`+${this.toolchain}`) } - return await batchExecuteWithProgress('lake', args, waitingPrompt, this.cwdUri?.fsPath, undefined, translator) + return await batchExecuteWithProgress('lake', args, waitingPrompt, { + cwd: this.cwdUri?.fsPath, + channel: this.channel, + translator, + allowCancellation: true + }) } } -export function lake(cwdUri: Uri | undefined, toolchain?: string | undefined): LakeRunner { - return new LakeRunner(cwdUri, toolchain) +export function lake(channel: OutputChannel, cwdUri: Uri | undefined, toolchain?: string | undefined): LakeRunner { + return new LakeRunner(channel, cwdUri, toolchain) } -export async function lakeInActiveFolder(toolchain?: string | undefined): Promise { +export async function lakeInActiveFolder(channel: OutputChannel, toolchain?: string | undefined): Promise { if (!window.activeTextEditor) { return 'NoActiveFolder' } @@ -81,5 +94,5 @@ export async function lakeInActiveFolder(toolchain?: string | undefined): Promis return 'NoActiveFolder' } - return lake(folderUri, toolchain) + return lake(channel, folderUri, toolchain) } diff --git a/vscode-lean4/src/utils/leanInstaller.ts b/vscode-lean4/src/utils/leanInstaller.ts index 6c3478251..ff961b1ad 100644 --- a/vscode-lean4/src/utils/leanInstaller.ts +++ b/vscode-lean4/src/utils/leanInstaller.ts @@ -224,7 +224,10 @@ export class LeanInstaller { // looks for a global (default) installation of Lean. This way, we can support // single file editing. logger.log(`executeWithProgress ${cmd} ${options}`) - const checkingResult: ExecutionResult = await batchExecuteWithProgress(cmd, options, 'Checking Lean setup...', folderPath, this.outputChannel) + const checkingResult: ExecutionResult = await batchExecuteWithProgress(cmd, options, 'Checking Lean setup...', { + cwd: folderPath, + channel: this.outputChannel + }) if (checkingResult.exitCode === ExecutionExitCode.CannotLaunch) { result.error = 'lean not found' } else if (checkingResult.stderr.indexOf('no default toolchain') > 0) { @@ -279,7 +282,7 @@ export class LeanInstaller { try { const cmd = 'elan'; const options = ['toolchain', 'list']; - const stdout = (await batchExecute(cmd, options, folderPath, undefined)).stdout + const stdout = (await batchExecute(cmd, options, folderPath)).stdout if (!stdout){ throw new Error('elan toolchain list returned no output.'); } diff --git a/vscode-lean4/src/utils/manifest.ts b/vscode-lean4/src/utils/manifest.ts new file mode 100644 index 000000000..e69de29bb From f658b431e3b73dc33a58f5e9c25eab538727c06b Mon Sep 17 00:00:00 2001 From: mhuisi Date: Fri, 6 Oct 2023 19:14:59 +0200 Subject: [PATCH 08/18] updateDependency command --- vscode-lean4/package.json | 24 +++- vscode-lean4/src/projectoperations.ts | 193 +++++++++++++++++++++++++- vscode-lean4/src/utils/batch.ts | 2 +- vscode-lean4/src/utils/lake.ts | 4 + vscode-lean4/src/utils/manifest.ts | 67 +++++++++ 5 files changed, 284 insertions(+), 6 deletions(-) diff --git a/vscode-lean4/package.json b/vscode-lean4/package.json index baf6d6027..66b50c1d5 100644 --- a/vscode-lean4/package.json +++ b/vscode-lean4/package.json @@ -352,6 +352,12 @@ "title": "Project: Clean Project", "description": "Clean the current project, removing all build artifacts" }, + { + "command": "lean4.project.updateDependency", + "category": "Lean 4", + "title": "Project: Update Dependency", + "description": "Updates a dependency of the current project to the most recent version available for the branch pinned in 'lakefile.lean'." + }, { "command": "lean4.project.fetchCache", "category": "Lean 4", @@ -519,10 +525,16 @@ "command": "lean4.project.clone" }, { - "command": "lean4.project.build" + "command": "lean4.project.build", + "when": "editorLangId == lean4" }, { - "command": "lean4.project.clean" + "command": "lean4.project.clean", + "when": "editorLangId == lean4" + }, + { + "command": "lean4.project.updateDependency", + "when": "editorLangId == lean4" }, { "command": "lean4.project.fetchCache", @@ -613,6 +625,11 @@ "when": "editorLangId == lean4", "group": "1_projectActions@2" }, + { + "command": "lean4.project.updateDependency", + "when": "editorLangId == lean4", + "group": "1_projectActions@3" + }, { "command": "lean4.project.fetchCache", "when": "editorLangId == lean4", @@ -861,7 +878,8 @@ "cheerio": "^1.0.0-rc.10", "mobx": "5.15.7", "semver": "=7.3.5", - "vscode-languageclient": "=8.0.2" + "vscode-languageclient": "=8.0.2", + "zod": "^3.22.4" }, "devDependencies": { "@types/cheerio": "~0.22.30", diff --git a/vscode-lean4/src/projectoperations.ts b/vscode-lean4/src/projectoperations.ts index 72b7fea86..1bdade71e 100644 --- a/vscode-lean4/src/projectoperations.ts +++ b/vscode-lean4/src/projectoperations.ts @@ -1,8 +1,12 @@ -import { Disposable, commands, window, OutputChannel } from 'vscode'; +import { Disposable, commands, window, OutputChannel, QuickPickItem, Uri } from 'vscode'; import { LakeRunner, cacheNotFoundError, lakeInActiveFolder } from './utils/lake'; -import { ExecutionExitCode, ExecutionResult, displayError } from './utils/batch'; +import { ExecutionExitCode, ExecutionResult, batchExecute, displayError } from './utils/batch'; import { LeanClientProvider } from './utils/clientProvider'; import { LeanClient } from './leanclient'; +import { findLeanPackageRoot } from './utils/projectInfo'; +import { join } from 'path'; +import * as fs from 'fs' +import { DirectGitDependency, Manifest, parseAsManifest } from './utils/manifest'; export class ProjectOperationProvider implements Disposable { @@ -13,6 +17,7 @@ export class ProjectOperationProvider implements Disposable { this.subscriptions.push( commands.registerCommand('lean4.project.build', () => this.buildProject()), commands.registerCommand('lean4.project.clean', () => this.cleanProject()), + commands.registerCommand('lean4.project.updateDependency', () => this.updateDependency()), commands.registerCommand('lean4.project.fetchCache', () => this.fetchMathlibCache()) ) } @@ -103,6 +108,187 @@ export class ProjectOperationProvider implements Disposable { }) } + private async updateDependency() { + if (!window.activeTextEditor) { + return + } + + const [_1, folderUri, _2] = await findLeanPackageRoot(window.activeTextEditor.document.uri) + if (!folderUri) { + return + } + + const manifestPath: string = join(folderUri.fsPath, 'lake-manifest.json') + + let jsonString: string + try { + jsonString = fs.readFileSync(manifestPath, 'utf8') // TODO: is this slow? + } catch (e) { + void window.showErrorMessage(`Cannot read 'lake-manifest.json' file at ${manifestPath} to determine dependencies.`) + return + } + + const manifest: Manifest | undefined = parseAsManifest(jsonString) + if (!manifest) { + void window.showErrorMessage(`Cannot parse 'lake-manifest.json' file at ${manifestPath} to determine dependencies.`) + return + } + + const dependencies: (DirectGitDependency & { remoteRevision?: string | undefined })[] = + await this.findUpdateableDependencies(manifest.directGitDependencies) + if (dependencies.length === 0) { + void window.showInformationMessage('Nothing to update - all dependencies are up-to-date.') + return + } + + const items: GitDependencyQuickPickItem[] = dependencies.map(gitDep => { + const shortLocalRevision: string = gitDep.revision.substring(0, 7) + const shortRemoteRevision: string | undefined = gitDep.remoteRevision?.substring(0, 7) + + const detail: string = shortRemoteRevision + ? `Current: ${shortLocalRevision} ⟹ New: ${shortRemoteRevision}` + : `Current: ${shortLocalRevision}` + + return { + label: `${gitDep.name} @ ${gitDep.inputRevision}`, + description: gitDep.uri.toString(), + detail, + ...gitDep + } + }) + + const dependencyChoice: GitDependencyQuickPickItem | undefined = await window.showQuickPick(items, { + title: 'Choose a dependency to update', + canPickMany: false + }) + if (!dependencyChoice) { + return + } + + const toolchainPathResult: [string, Uri] | 'DoNotUpdate' | 'Cancelled' = await this.determineToolchainPathsToUpdate(folderUri, dependencyChoice) + if (toolchainPathResult === 'Cancelled') { + return + } + + await this.runOperation(async lakeRunner => { + if (toolchainPathResult !== 'DoNotUpdate') { + const [localToolchainPath, dependencyToolchainUri] = toolchainPathResult + + const curlResult: ExecutionResult = await batchExecute('curl', ['-f', '-L', dependencyToolchainUri.toString(), '-o', localToolchainPath]) + if (curlResult.exitCode !== ExecutionExitCode.Success) { + void window.showErrorMessage('Cannot update Lean version.') + return + } + } + + const result: ExecutionResult = await lakeRunner.updateDependency(dependencyChoice.name) + if (result.exitCode === ExecutionExitCode.Cancelled) { + return + } + if (result.exitCode !== ExecutionExitCode.Success) { + void window.showErrorMessage('Cannot update dependency.') + return + } + }) + } + + private async findUpdateableDependencies(dependencies: DirectGitDependency[]) { + const augmented: (DirectGitDependency & { remoteRevision?: string | undefined })[] = [] + + for (const dependency of dependencies) { + const result: ExecutionResult = await batchExecute('git', ['ls-remote', dependency.uri.toString(), dependency.inputRevision]) + if (result.exitCode !== ExecutionExitCode.Success) { + augmented.push(dependency) + continue + } + + const matches: RegExpMatchArray | null = result.stdout.match(/^[a-z0-9]+/) + if (!matches) { + augmented.push(dependency) + continue + } + + const remoteRevision: string = matches[0] + if (dependency.revision === remoteRevision) { + // Cannot be updated - filter it + continue + } + + augmented.push({ remoteRevision, ...dependency }) + } + + return augmented + } + + private async determineToolchainPathsToUpdate(rootFolderUri: Uri, dependency: DirectGitDependency): Promise<[string, Uri] | 'DoNotUpdate' | 'Cancelled'> { + const localToolchainPath: string = join(rootFolderUri.fsPath, 'lean-toolchain') + const dependencyToolchainUri: Uri | undefined = this.determineDependencyToolchainUri(dependency.uri, dependency.inputRevision) + if (!dependencyToolchainUri) { + return 'DoNotUpdate' + } + + const toolchainResult = await this.fetchToolchains(localToolchainPath, dependencyToolchainUri) + if (toolchainResult === undefined) { + return 'DoNotUpdate' + } + const [localToolchain, dependencyToolchain]: [string, string] = toolchainResult + + if (localToolchain === dependencyToolchain) { + return 'DoNotUpdate' + } + + const message = `Local Lean version '${localToolchain}' differs from Lean version of ${dependency.name} '${dependencyToolchain}'. Do you want to update the local Lean version to the version of ${dependency.name}?` + const input1 = 'Update Local Version' + const input2 = 'Keep Local Version' + const choice = await window.showInformationMessage(message, { modal: true }, input1, input2) + if (choice === undefined) { + return 'Cancelled' + } + if (choice !== input1) { + return 'DoNotUpdate' + } + + return [localToolchainPath, dependencyToolchainUri] + } + + private determineDependencyToolchainUri(dependencyUri: Uri, inputRevision: string): Uri | undefined { + // Example: + // Input: https://github.com/leanprover-community/mathlib4 + // Output: https://raw.githubusercontent.com/leanprover-community/mathlib4/master/lean-toolchain + + if (!dependencyUri.authority.includes('github.com')) { + return undefined + } + const match = dependencyUri.path.match(/\/([^\\]+\/[^\\\.]+)(\.git)?\/?/) + if (!match) { + return undefined + } + const repoPath: string = match[1] + + return Uri.from({ + scheme: 'https', + authority: 'raw.githubusercontent.com', + path: join(repoPath, inputRevision, 'lean-toolchain') + }) + } + + private async fetchToolchains(localToolchainPath: string, dependencyToolchainUri: Uri): Promise<[string, string] | undefined> { + let localToolchain: string + try { + localToolchain = fs.readFileSync(localToolchainPath, 'utf8').trim() + } catch (e) { + return undefined + } + + const curlResult: ExecutionResult = await batchExecute('curl', ['-f', '-L', dependencyToolchainUri.toString()]) + if (curlResult.exitCode !== ExecutionExitCode.Success) { + return undefined + } + const dependencyToolchain: string = curlResult.stdout.trim() + + return [localToolchain, dependencyToolchain] + } + private async runOperation(command: (lakeRunner: LakeRunner) => Promise) { if (this.isRunningOperation) { void window.showErrorMessage('Another project action is already being executed. Please wait for its completion.') @@ -142,3 +328,6 @@ export class ProjectOperationProvider implements Disposable { } } + +interface GitDependencyQuickPickItem extends QuickPickItem, DirectGitDependency { +} diff --git a/vscode-lean4/src/utils/batch.ts b/vscode-lean4/src/utils/batch.ts index 1eb417b02..047af5354 100644 --- a/vscode-lean4/src/utils/batch.ts +++ b/vscode-lean4/src/utils/batch.ts @@ -148,7 +148,7 @@ export async function batchExecuteWithProgress( value = translatedValue } if (options.channel) { - options.channel.appendLine(value) + options.channel.appendLine(value.trimEnd()) } if (inc < 90) { inc += 2 diff --git a/vscode-lean4/src/utils/lake.ts b/vscode-lean4/src/utils/lake.ts index b4c9bd6f6..a95651db1 100644 --- a/vscode-lean4/src/utils/lake.ts +++ b/vscode-lean4/src/utils/lake.ts @@ -23,6 +23,10 @@ export class LakeRunner { return this.runLakeCommandWithProgress('update', [], 'Updating dependencies ...') } + async updateDependency(dependencyName: string): Promise { + return this.runLakeCommandWithProgress('update', [dependencyName], `Updating '${dependencyName}' dependency ...`) + } + async build(): Promise { return this.runLakeCommandWithProgress('build', [], 'Building Lean project ...') } diff --git a/vscode-lean4/src/utils/manifest.ts b/vscode-lean4/src/utils/manifest.ts index e69de29bb..1085b7b86 100644 --- a/vscode-lean4/src/utils/manifest.ts +++ b/vscode-lean4/src/utils/manifest.ts @@ -0,0 +1,67 @@ +import { Uri } from 'vscode' +import { z } from 'zod'; + +export interface DirectGitDependency { + name: string + uri: Uri + revision: string + inputRevision: string +} + +export interface Manifest { + directGitDependencies: DirectGitDependency[] +} + +export function parseAsManifest(jsonString: string): Manifest | undefined { + let parsedJson: any + try { + parsedJson = JSON.parse(jsonString) + } catch (e) { + return undefined + } + + const schema = z.object({ + packages: z.array( + z.union([ + z.object({ + git: z.object({ + name: z.string(), + url: z.string().url(), + rev: z.string(), + inherited: z.boolean(), + 'inputRev?': z.optional(z.nullable(z.string())) + }) + }), + z.object({ + path: z.any() + }) + ]) + ) + }) + const result = schema.safeParse(parsedJson) + if (!result.success) { + return undefined + } + + const manifest: Manifest = { directGitDependencies: [] } + + for (const pkg of result.data.packages) { + if (!('git' in pkg)) { + continue + } + if (pkg.git.inherited) { + continue // Inherited Git packages are not direct dependencies + } + + const inputRev: string | null | undefined = pkg.git['inputRev?'] + + manifest.directGitDependencies.push({ + name: pkg.git.name, + uri: Uri.parse(pkg.git.url), + revision: pkg.git.rev, + inputRevision: inputRev ? inputRev : 'master' // Lake also always falls back to master + }) + } + + return manifest +} From 26fad366ccb112b20b2ec6e4e9860b6835fe8aaa Mon Sep 17 00:00:00 2001 From: mhuisi Date: Tue, 10 Oct 2023 14:29:19 +0200 Subject: [PATCH 09/18] smaller improvements - standalone vs mathlib project creation - expose show output and install elan command - prevent showing "restart server" message while already restarting it - prevent client start crash softlocking the restart - client start progress bar - 'show output' button in command errors - use stable toolchain for new projects - write toolchain directly to file instead of doing so via re-curling - fetch cache after dependency update - better error messages for non-github projects - better output formatting --- vscode-lean4/media/guide-setupProject.md | 12 ++-- vscode-lean4/package.json | 77 ++++++++++++---------- vscode-lean4/src/extension.ts | 20 ++++-- vscode-lean4/src/leanclient.ts | 41 ++++++++---- vscode-lean4/src/projectinit.ts | 24 ++----- vscode-lean4/src/projectoperations.ts | 83 +++++++++++++---------- vscode-lean4/src/utils/batch.ts | 36 ++++++---- vscode-lean4/src/utils/clientProvider.ts | 14 ++-- vscode-lean4/src/utils/errors.ts | 9 +++ vscode-lean4/src/utils/lake.ts | 5 +- vscode-lean4/src/utils/leanInstaller.ts | 50 +++++++------- vscode-lean4/src/utils/leanpkg.ts | 84 ++++++++++++++---------- vscode-lean4/src/utils/manifest.ts | 24 ++++++- 13 files changed, 282 insertions(+), 197 deletions(-) create mode 100644 vscode-lean4/src/utils/errors.ts diff --git a/vscode-lean4/media/guide-setupProject.md b/vscode-lean4/media/guide-setupProject.md index 6ce0ef0e3..53648a304 100644 --- a/vscode-lean4/media/guide-setupProject.md +++ b/vscode-lean4/media/guide-setupProject.md @@ -1,11 +1,9 @@ ## Project Creation If you want to create a new project, click on one of the following: -- [Create a new library project](command:lean4.project.createLibraryProject) - Library projects can be used by other Lean 4 projects. -- [Create a new math formalization project](command:lean4.project.createMathlibProject) - Math formalization projects are library projects that depend on [mathlib](https://github.com/leanprover-community/mathlib4), the math library of Lean 4. -- [Create a new program project](command:lean4.project.createProgramProject) - Program projects allow compiling Lean code to executable programs. +- [Create a new standalone project](command:lean4.project.createStandaloneProject) + Standalone projects do not depend on any other Lean 4 projects. Dependencies can be added by modifying 'lakefile.lean' in the newly created project as described [here](https://github.com/leanprover/lean4/blob/master/src/lake/README.md#adding-dependencies). +- [Create a new mathlib project](command:lean4.project.createMathlibProject) + Mathlib projects depend on [mathlib](https://github.com/leanprover-community/mathlib4), the math library of Lean 4. If you want to open an existing project, click on one of the following: - [Download an existing project](command:lean4.project.clone) @@ -14,4 +12,4 @@ If you want to open an existing project, click on one of the following: After creating or downloading a project, you can open it in the future by clicking the ∀-symbol in the top right, choosing 'Open Project…' > 'Open Local Project…' and selecting the project you created. ## Complex Project Setups -Using its build system and package manager Lake, Lean 4 supports project setups that are more complex than the ones above. You can find out more about Lake in the [Lean 4 GitHub repository](https://github.com/leanprover/lean4/blob/master/src/lake/README.md). +Using its build system and package manager Lake, Lean 4 supports more complex project setups than the ones described above. You can find out more about Lake in the [Lean 4 GitHub repository](https://github.com/leanprover/lean4/blob/master/src/lake/README.md). diff --git a/vscode-lean4/package.json b/vscode-lean4/package.json index 66b50c1d5..da5ce3276 100644 --- a/vscode-lean4/package.json +++ b/vscode-lean4/package.json @@ -293,16 +293,16 @@ "description": "Go to next page in documentation view" }, { - "command": "lean4.showOutput", + "command": "lean4.troubleshooting.showOutput", "category": "Lean 4", - "title": "Show Output", + "title": "Troubleshooting: Show Output", "description": "Show output channel containing all progress updates and errors of commands" }, { "command": "lean4.setup.showSetupGuide", "category": "Lean 4", "title": "Setup: Show Setup Guide", - "description": "Show 'Welcome' page containing a checklist of steps to install Lean 4." + "description": "Show 'Welcome' page containing a checklist of steps to install Lean 4" }, { "command": "lean4.setup.installElan", @@ -311,22 +311,16 @@ "description": "Install Lean's version manager 'Elan'" }, { - "command": "lean4.project.createLibraryProject", + "command": "lean4.project.createStandaloneProject", "category": "Lean 4", - "title": "Project: Create Library Project…", - "description": "Create a new Lean library project" - }, - { - "command": "lean4.project.createProgramProject", - "category": "Lean 4", - "title": "Project: Create Program Project…", - "description": "Create a new Lean program project" + "title": "Project: Create Standalone Project…", + "description": "Create a new Lean project that does not depend on any other projects" }, { "command": "lean4.project.createMathlibProject", "category": "Lean 4", - "title": "Project: Create Math Formalization Project…", - "description": "Create a new Lean math formalization project" + "title": "Project: Create Mathlib Project…", + "description": "Create a new Lean math formalization project using Mathlib" }, { "command": "lean4.project.open", @@ -355,7 +349,7 @@ { "command": "lean4.project.updateDependency", "category": "Lean 4", - "title": "Project: Update Dependency", + "title": "Project: Update Dependency…", "description": "Updates a dependency of the current project to the most recent version available for the branch pinned in 'lakefile.lean'." }, { @@ -501,7 +495,7 @@ "command": "lean4.docView.forward" }, { - "command": "lean4.showOutput" + "command": "lean4.troubleshooting.showOutput" }, { "command": "lean4.setup.showSetupGuide" @@ -510,10 +504,7 @@ "command": "lean4.setup.installElan" }, { - "command": "lean4.project.createLibraryProject" - }, - { - "command": "lean4.project.createProgramProject" + "command": "lean4.project.createStandaloneProject" }, { "command": "lean4.project.createMathlibProject" @@ -574,28 +565,33 @@ "when": "editorLangId == lean4", "group": "3_infoview@1" }, + { + "command": "lean4.troubleshooting.showOutput", + "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", + "group": "4_troubleshooting" + }, + { + "submenu": "lean4.titlebar.versions", + "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", + "group": "5_versions" + }, { "submenu": "lean4.titlebar.projectActions", "when": "editorLangId == lean4", - "group": "4_projectActions@1" + "group": "6_projectActions@1" }, { "submenu": "lean4.titlebar.documentation", "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", - "group": "5_documentation@1" + "group": "7_documentation@1" } ], "lean4.titlebar.newProject": [ { - "command": "lean4.project.createLibraryProject", + "command": "lean4.project.createStandaloneProject", "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", "group": "1_newProject@1" }, - { - "command": "lean4.project.createProgramProject", - "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", - "group": "1_newProject@2" - }, { "command": "lean4.project.createMathlibProject", "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", @@ -614,6 +610,13 @@ "group": "1_openProject@2" } ], + "lean4.titlebar.versions": [ + { + "command": "lean4.setup.installElan", + "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", + "group": "1_setup@1" + } + ], "lean4.titlebar.projectActions": [ { "command": "lean4.project.build", @@ -678,13 +681,17 @@ "id": "lean4.titlebar.openProject", "label": "Open Project…" }, + { + "id": "lean4.titlebar.versions", + "label": "Version Management…" + }, { "id": "lean4.titlebar.projectActions", "label": "Project Actions…" }, { "id": "lean4.titlebar.documentation", - "label": "Show Documentation…" + "label": "Documentation…" } ], "semanticTokenScopes": [ @@ -718,10 +725,10 @@ { "id": "guide.linux.openSetupGuide", "title": "Re-Open Setup Guide", - "description": "This guide can always be re-opened by opening an empty file, clicking on the ∀-symbol in the top right and selecting 'Show Documentation…' > 'Setup: Show Setup Guide'.", + "description": "This guide can always be re-opened by opening an empty file, clicking on the ∀-symbol in the top right and selecting 'Documentation…' > 'Setup: Show Setup Guide'.", "media": { "image": "media/open-setup-guide.png", - "altText": "Click on the ∀-symbol in the top right and select 'Show Documentation…' > 'Setup: Show Setup Guide'." + "altText": "Click on the ∀-symbol in the top right and select 'Documentation…' > 'Setup: Show Setup Guide'." } }, { @@ -765,10 +772,10 @@ { "id": "guide.mac.openSetupGuide", "title": "Re-Open Setup Guide", - "description": "This guide can always be re-opened by opening an empty file, clicking on the ∀-symbol in the top right and selecting 'Show Documentation…' > 'Setup: Show Setup Guide'.", + "description": "This guide can always be re-opened by opening an empty file, clicking on the ∀-symbol in the top right and selecting 'Documentation…' > 'Setup: Show Setup Guide'.", "media": { "image": "media/open-setup-guide.png", - "altText": "Click on the ∀-symbol in the top right and select 'Show Documentation…' > 'Setup: Show Setup Guide'." + "altText": "Click on the ∀-symbol in the top right and select 'Documentation…' > 'Setup: Show Setup Guide'." } }, { @@ -812,10 +819,10 @@ { "id": "guide.windows.openSetupGuide", "title": "Re-Open Setup Guide", - "description": "This guide can always be re-opened by opening an empty file, clicking on the ∀-symbol in the top right and selecting 'Show Documentation…' > 'Setup: Show Setup Guide'.", + "description": "This guide can always be re-opened by opening an empty file, clicking on the ∀-symbol in the top right and selecting 'Documentation…' > 'Setup: Show Setup Guide'.", "media": { "image": "media/open-setup-guide.png", - "altText": "Click on the ∀-symbol in the top right and select 'Show Documentation…' > 'Setup: Show Setup Guide'." + "altText": "Click on the ∀-symbol in the top right and select 'Documentation…' > 'Setup: Show Setup Guide'." } }, { diff --git a/vscode-lean4/src/extension.ts b/vscode-lean4/src/extension.ts index 1aa930915..738bb9a01 100644 --- a/vscode-lean4/src/extension.ts +++ b/vscode-lean4/src/extension.ts @@ -12,6 +12,7 @@ import { Exports } from './exports'; import { logger } from './utils/logger' import { ProjectInitializationProvider } from './projectinit' import { ProjectOperationProvider } from './projectoperations' +import { LeanClient } from './leanclient' interface AlwaysEnabledFeatures { docView: DocViewProvider @@ -76,7 +77,7 @@ function activateAlwaysEnabledFeatures(context: ExtensionContext): AlwaysEnabled context.subscriptions.push(docView); const outputChannel = window.createOutputChannel('Lean: Editor'); - context.subscriptions.push(commands.registerCommand('lean4.showOutput', () => outputChannel.show(true))) + context.subscriptions.push(commands.registerCommand('lean4.troubleshooting.showOutput', () => outputChannel.show(true))) const projectInitializationProvider = new ProjectInitializationProvider(outputChannel) context.subscriptions.push(projectInitializationProvider) @@ -116,14 +117,23 @@ function activateAbbreviationFeature(context: ExtensionContext, docView: DocView } function activateLean4Features(context: ExtensionContext, installer: LeanInstaller): Lean4EnabledFeatures { + const clientProvider = new LeanClientProvider(installer, installer.getOutputChannel()); + context.subscriptions.push(clientProvider) + const pkgService = new LeanpkgService() - pkgService.versionChanged((uri) => installer.handleVersionChanged(uri)); + pkgService.versionChanged(async uri => { + const client: LeanClient | undefined = clientProvider.getClientForFolder(uri) + if (client && !client.isRunning()) { + // This can naturally happen when we update the Lean version using the "Update Dependency" command + // because the Lean server is stopped while doing so. We want to avoid triggering the "Version changed" + // message in this case. + return + } + await installer.handleVersionChanged(uri) + }); pkgService.lakeFileChanged((uri) => installer.handleLakeFileChanged(uri)); context.subscriptions.push(pkgService); - const clientProvider = new LeanClientProvider(installer, pkgService, installer.getOutputChannel()); - context.subscriptions.push(clientProvider) - const infoProvider = new InfoProvider(clientProvider, {language: 'lean4'}, context); context.subscriptions.push(infoProvider) diff --git a/vscode-lean4/src/leanclient.ts b/vscode-lean4/src/leanclient.ts index 9f3032263..2f8036d3b 100644 --- a/vscode-lean4/src/leanclient.ts +++ b/vscode-lean4/src/leanclient.ts @@ -1,7 +1,7 @@ import { TextDocument, EventEmitter, Diagnostic, DocumentHighlight, Range, DocumentHighlightKind, workspace, Disposable, Uri, ConfigurationChangeEvent, OutputChannel, DiagnosticCollection, - WorkspaceFolder, window } from 'vscode' + WorkspaceFolder, window, ProgressLocation, ProgressOptions, Progress } from 'vscode' import { DidChangeTextDocumentParams, DidCloseTextDocumentParams, @@ -31,6 +31,7 @@ import { SemVer } from 'semver'; import { fileExists, isFileInFolder } from './utils/fsHelper'; import { c2pConverter, p2cConverter, patchConverters } from './utils/converters' import { Server } from 'http' +import { displayErrorWithOutput } from './utils/errors' const escapeRegExp = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); @@ -138,16 +139,33 @@ export class LeanClient implements Disposable { return } this.isRestarting = true - const startTime = Date.now() + try { + logger.log('[LeanClient] Restarting Lean Server') + if (this.isStarted()) { + await this.stop() + } - logger.log('[LeanClient] Restarting Lean Server') - if (this.isStarted()) { - await this.stop() + this.restartingEmitter.fire(undefined) + this.toolchainPath = toolchainPath(); + + const progressOptions: ProgressOptions = { + location: ProgressLocation.Notification, + title: '', + cancellable: false + } + await window.withProgress(progressOptions, async progress => + await this.startClient(progress)) + } finally { + this.isRestarting = false } + } - this.restartingEmitter.fire(undefined) - this.toolchainPath = toolchainPath(); + private async startClient(progress: Progress<{ message?: string; increment?: number }>) { + // Should only be called from `restart` + const startTime = Date.now() + const message = 'Starting Lean language client ...' + progress.report({ message, increment: 0 }) this.client = await this.setupClient() let insideRestart = true; @@ -174,6 +192,7 @@ export class LeanClient implements Disposable { } } }) + progress.report({ message, increment: 80 }) await this.client.start() // tell the new client about the documents that are already open! for (const key of this.isOpen.keys()) { @@ -188,7 +207,6 @@ export class LeanClient implements Disposable { this.outputChannel.appendLine(msg); this.serverFailedEmitter.fire(msg); insideRestart = false; - this.isRestarting = false return; } @@ -219,18 +237,13 @@ export class LeanClient implements Disposable { this.client?.outputChannel.show(true); } else if (!stderrMsgBoxVisible) { stderrMsgBoxVisible = true; - const outputItem = 'Show stderr output'; - const outPrompt = `Lean server printed an error:\n${chunk.toString()}`; - if (await window.showErrorMessage(outPrompt, outputItem) === outputItem) { - this.outputChannel.show(false); - } + await displayErrorWithOutput(`Lean server printed an error:\n${chunk.toString()}`) stderrMsgBoxVisible = false; } }); this.restartedEmitter.fire(undefined) insideRestart = false; - this.isRestarting = false } async withStoppedClient(action: () => Promise): Promise<'Success' | 'IsRestarting'> { diff --git a/vscode-lean4/src/projectinit.ts b/vscode-lean4/src/projectinit.ts index 8ffdd7b29..526aee48e 100644 --- a/vscode-lean4/src/projectinit.ts +++ b/vscode-lean4/src/projectinit.ts @@ -11,24 +11,15 @@ export class ProjectInitializationProvider implements Disposable { constructor(private channel: OutputChannel) { this.subscriptions.push( - commands.registerCommand('lean4.project.createLibraryProject', () => this.createLibraryProject()), - commands.registerCommand('lean4.project.createProgramProject', () => this.createProgramProject()), + commands.registerCommand('lean4.project.createStandaloneProject', () => this.createStandaloneProject()), commands.registerCommand('lean4.project.createMathlibProject', () => this.createMathlibProject()), commands.registerCommand('lean4.project.open', () => this.openProject()), commands.registerCommand('lean4.project.clone', () => this.cloneProject()) ) } - private async createLibraryProject() { - const projectFolder: Uri | 'DidNotComplete' = await this.createProject('lib', 'library', 'stable') - - if (projectFolder !== 'DidNotComplete') { - await ProjectInitializationProvider.openNewFolder(projectFolder) - } - } - - private async createProgramProject() { - const projectFolder: Uri | 'DidNotComplete' = await this.createProject('exe', 'program', 'stable') + private async createStandaloneProject() { + const projectFolder: Uri | 'DidNotComplete' = await this.createProject() if (projectFolder !== 'DidNotComplete') { await ProjectInitializationProvider.openNewFolder(projectFolder) @@ -37,7 +28,7 @@ export class ProjectInitializationProvider implements Disposable { private async createMathlibProject() { const mathlibToolchain = 'leanprover-community/mathlib4:lean-toolchain' - const projectFolder: Uri | 'DidNotComplete' = await this.createProject('math', 'math formalization', mathlibToolchain) + const projectFolder: Uri | 'DidNotComplete' = await this.createProject('math', mathlibToolchain) if (projectFolder === 'DidNotComplete') { return @@ -65,13 +56,12 @@ export class ProjectInitializationProvider implements Disposable { } private async createProject( - kind: string, - kindName: string, - toolchain?: string | undefined): Promise { + kind?: string | undefined, + toolchain: string = 'leanprover/lean4:stable'): Promise { const projectFolder: Uri | undefined = await ProjectInitializationProvider.askForNewProjectFolderLocation({ saveLabel: 'Create project folder', - title: `Create a new ${kindName} project folder` + title: 'Create a new project folder' }) if (projectFolder === undefined) { return 'DidNotComplete' diff --git a/vscode-lean4/src/projectoperations.ts b/vscode-lean4/src/projectoperations.ts index 1bdade71e..63df86f34 100644 --- a/vscode-lean4/src/projectoperations.ts +++ b/vscode-lean4/src/projectoperations.ts @@ -6,7 +6,7 @@ import { LeanClient } from './leanclient'; import { findLeanPackageRoot } from './utils/projectInfo'; import { join } from 'path'; import * as fs from 'fs' -import { DirectGitDependency, Manifest, parseAsManifest } from './utils/manifest'; +import { DirectGitDependency, Manifest, ManifestReadError, parseAsManifest, parseManifestInFolder } from './utils/manifest'; export class ProjectOperationProvider implements Disposable { @@ -24,9 +24,8 @@ export class ProjectOperationProvider implements Disposable { private async buildProject() { await this.runOperation(async lakeRunner => { - // Try it. If this is not a mathlib project, it will fail silently. Otherwise, it will grab the cache. - const fetchResult: ExecutionResult = await lakeRunner.fetchMathlibCache(true) - if (fetchResult.exitCode === ExecutionExitCode.Cancelled) { + const fetchResult: 'Success' | 'CacheNotAvailable' | 'Cancelled' = await this.tryFetchingCache(lakeRunner) + if (fetchResult === 'Cancelled') { return } @@ -118,24 +117,14 @@ export class ProjectOperationProvider implements Disposable { return } - const manifestPath: string = join(folderUri.fsPath, 'lake-manifest.json') - - let jsonString: string - try { - jsonString = fs.readFileSync(manifestPath, 'utf8') // TODO: is this slow? - } catch (e) { - void window.showErrorMessage(`Cannot read 'lake-manifest.json' file at ${manifestPath} to determine dependencies.`) - return - } - - const manifest: Manifest | undefined = parseAsManifest(jsonString) - if (!manifest) { - void window.showErrorMessage(`Cannot parse 'lake-manifest.json' file at ${manifestPath} to determine dependencies.`) + const manifestResult: Manifest | ManifestReadError = await parseManifestInFolder(folderUri) + if (typeof manifestResult === 'string') { + void window.showErrorMessage(manifestResult) return } const dependencies: (DirectGitDependency & { remoteRevision?: string | undefined })[] = - await this.findUpdateableDependencies(manifest.directGitDependencies) + await this.findUpdateableDependencies(manifestResult.directGitDependencies) if (dependencies.length === 0) { void window.showInformationMessage('Nothing to update - all dependencies are up-to-date.') return @@ -165,17 +154,17 @@ export class ProjectOperationProvider implements Disposable { return } - const toolchainPathResult: [string, Uri] | 'DoNotUpdate' | 'Cancelled' = await this.determineToolchainPathsToUpdate(folderUri, dependencyChoice) - if (toolchainPathResult === 'Cancelled') { + const localToolchainPath: string = join(folderUri.fsPath, 'lean-toolchain') + const dependencyToolchainResult: string | 'DoNotUpdate' | 'Cancelled' = await this.determineDependencyToolchain(localToolchainPath, dependencyChoice) + if (dependencyToolchainResult === 'Cancelled') { return } await this.runOperation(async lakeRunner => { - if (toolchainPathResult !== 'DoNotUpdate') { - const [localToolchainPath, dependencyToolchainUri] = toolchainPathResult - - const curlResult: ExecutionResult = await batchExecute('curl', ['-f', '-L', dependencyToolchainUri.toString(), '-o', localToolchainPath]) - if (curlResult.exitCode !== ExecutionExitCode.Success) { + if (dependencyToolchainResult !== 'DoNotUpdate') { + try { + fs.writeFileSync(localToolchainPath, dependencyToolchainResult) + } catch { void window.showErrorMessage('Cannot update Lean version.') return } @@ -186,9 +175,11 @@ export class ProjectOperationProvider implements Disposable { return } if (result.exitCode !== ExecutionExitCode.Success) { - void window.showErrorMessage('Cannot update dependency.') + void displayError(result, 'Cannot update dependency.') return } + + await this.tryFetchingCache(lakeRunner) }) } @@ -220,16 +211,24 @@ export class ProjectOperationProvider implements Disposable { return augmented } - private async determineToolchainPathsToUpdate(rootFolderUri: Uri, dependency: DirectGitDependency): Promise<[string, Uri] | 'DoNotUpdate' | 'Cancelled'> { - const localToolchainPath: string = join(rootFolderUri.fsPath, 'lean-toolchain') + private async determineDependencyToolchain(localToolchainPath: string, dependency: DirectGitDependency): Promise { const dependencyToolchainUri: Uri | undefined = this.determineDependencyToolchainUri(dependency.uri, dependency.inputRevision) if (!dependencyToolchainUri) { - return 'DoNotUpdate' + const message = `Could not determine Lean version of ${dependency.name} at ${dependency.uri}, as doing so is currently only supported for GitHub projects. Do you want to update ${dependency.name} without updating the local Lean version to that of ${dependency.name} regardless?` + const input = 'Proceed' + const choice: string | undefined = await window.showInformationMessage(message, { modal: true}, input) + return choice === 'input' ? 'DoNotUpdate' : 'Cancelled' } const toolchainResult = await this.fetchToolchains(localToolchainPath, dependencyToolchainUri) - if (toolchainResult === undefined) { - return 'DoNotUpdate' + if (!(toolchainResult instanceof Array)) { + const errorFlavor = toolchainResult === 'CannotReadLocalToolchain' + ? `Could not read local Lean version at '${localToolchainPath}'` + : `Could not fetch Lean version of ${dependency.name} at ${dependency.uri}` + const message = `${errorFlavor}. Do you want to update ${dependency.name} without updating the local Lean version to that of ${dependency.name} regardless?` + const input = 'Proceed' + const choice: string | undefined = await window.showInformationMessage(message, { modal: true}, input) + return choice === 'input' ? 'DoNotUpdate' : 'Cancelled' } const [localToolchain, dependencyToolchain]: [string, string] = toolchainResult @@ -237,7 +236,7 @@ export class ProjectOperationProvider implements Disposable { return 'DoNotUpdate' } - const message = `Local Lean version '${localToolchain}' differs from Lean version of ${dependency.name} '${dependencyToolchain}'. Do you want to update the local Lean version to the version of ${dependency.name}?` + const message = `'${localToolchain}' (local Lean version) differs from '${dependencyToolchain}' (${dependency.name} Lean version). Do you want to update the local Lean version to the Lean version of ${dependency.name}?` const input1 = 'Update Local Version' const input2 = 'Keep Local Version' const choice = await window.showInformationMessage(message, { modal: true }, input1, input2) @@ -248,7 +247,7 @@ export class ProjectOperationProvider implements Disposable { return 'DoNotUpdate' } - return [localToolchainPath, dependencyToolchainUri] + return dependencyToolchain } private determineDependencyToolchainUri(dependencyUri: Uri, inputRevision: string): Uri | undefined { @@ -272,23 +271,35 @@ export class ProjectOperationProvider implements Disposable { }) } - private async fetchToolchains(localToolchainPath: string, dependencyToolchainUri: Uri): Promise<[string, string] | undefined> { + private async fetchToolchains(localToolchainPath: string, dependencyToolchainUri: Uri): Promise<[string, string] | 'CannotReadLocalToolchain' | 'CannotReadDependencyToolchain'> { let localToolchain: string try { localToolchain = fs.readFileSync(localToolchainPath, 'utf8').trim() } catch (e) { - return undefined + return 'CannotReadLocalToolchain' } const curlResult: ExecutionResult = await batchExecute('curl', ['-f', '-L', dependencyToolchainUri.toString()]) if (curlResult.exitCode !== ExecutionExitCode.Success) { - return undefined + return 'CannotReadDependencyToolchain' } const dependencyToolchain: string = curlResult.stdout.trim() return [localToolchain, dependencyToolchain] } + private async tryFetchingCache(lakeRunner: LakeRunner): Promise<'Success' | 'CacheNotAvailable' | 'Cancelled'> { + const fetchResult: ExecutionResult = await lakeRunner.fetchMathlibCache(true) + switch (fetchResult.exitCode) { + case ExecutionExitCode.Success: + return 'Success' + case ExecutionExitCode.Cancelled: + return 'Cancelled' + default: + return 'CacheNotAvailable' + } + } + private async runOperation(command: (lakeRunner: LakeRunner) => Promise) { if (this.isRunningOperation) { void window.showErrorMessage('Another project action is already being executed. Please wait for its completion.') diff --git a/vscode-lean4/src/utils/batch.ts b/vscode-lean4/src/utils/batch.ts index 047af5354..3fecfcd94 100644 --- a/vscode-lean4/src/utils/batch.ts +++ b/vscode-lean4/src/utils/batch.ts @@ -2,6 +2,7 @@ import { CancellationToken, Disposable, OutputChannel, ProgressLocation, Progres import { spawn } from 'child_process'; import { findProgramInPath, isRunningTest } from '../config' import { logger } from './logger' +import { displayErrorWithOutput } from './errors'; export interface ExecutionChannel { combined?: OutputChannel | undefined @@ -46,8 +47,7 @@ export async function batchExecute( } try { - if (isRunningTest()) - { + if (isRunningTest()) { // The mocha test framework listens to process.on('uncaughtException') // which is raised if spawn cannot find the command and the test automatically // fails with "Uncaught Error: spawn elan ENOENT". Therefore we manually @@ -58,6 +58,11 @@ export async function batchExecute( return; } } + if (channel?.combined) { + const formattedCwd = workingDirectory ? `${workingDirectory}` : '' + const formattedArgs = args.map(arg => arg.includes(' ') ? `"${arg}"` : arg).join(' ') + channel.combined.appendLine(`${formattedCwd}> ${executablePath} ${formattedArgs}`) + } const proc = spawn(executablePath, args, options); const disposeKill: Disposable | undefined = token?.onCancellationRequested(_ => proc.kill()) @@ -69,15 +74,15 @@ export async function batchExecute( proc.stdout.on('data', (line) => { const s: string = line.toString(); - if (channel && channel.combined) channel.combined.appendLine(s) - if (channel && channel.stdout) channel.stdout.appendLine(s) + if (channel?.combined) channel.combined.appendLine(s) + if (channel?.stdout) channel.stdout.appendLine(s) stdout += s + '\n'; }); proc.stderr.on('data', (line) => { const s: string = line.toString(); - if (channel && channel.combined) channel.combined.appendLine(s) - if (channel && channel.stderr) channel.stderr.appendLine(s) + if (channel?.combined) channel.combined.appendLine(s) + if (channel?.stderr) channel.stderr.appendLine(s) stderr += s + '\n'; }); @@ -85,6 +90,9 @@ export async function batchExecute( disposeKill?.dispose() logger.log(`child process exited with code ${code}`); if (signal === 'SIGTERM') { + if (channel?.combined) { + channel.combined.appendLine('=> Operation cancelled by user.') + } resolve({ exitCode: ExecutionExitCode.Cancelled, stdout, @@ -93,6 +101,11 @@ export async function batchExecute( return } if (code !== 0) { + if (channel?.combined) { + const formattedCode = code ? `Exit code: ${code}.` : '' + const formattedSignal = signal ? `Signal: ${signal}.` : '' + channel.combined.appendLine(`=> Operation failed. ${formattedCode} ${formattedSignal}`.trim()) + } resolve({ exitCode: ExecutionExitCode.ExecutionError, stdout, @@ -127,7 +140,7 @@ export async function batchExecuteWithProgress( prompt: string, options: ProgressExecutionOptions = {}): Promise { - const messagePrefix = options.channel ? '[(Details)](command:lean4.showOutput) ' : '' + const messagePrefix = options.channel ? '[(Details)](command:lean4.troubleshooting.showOutput) ' : '' const progressOptions: ProgressOptions = { location: ProgressLocation.Notification, @@ -189,20 +202,17 @@ export async function executeAll(executions: BatchExecution[]): Promise = new Map(); private clients: Map = new Map(); private pending: Map = new Map(); @@ -34,18 +33,17 @@ export class LeanClientProvider implements Disposable { private clientStoppedEmitter = new EventEmitter<[LeanClient, boolean, ServerStoppedReason]>() clientStopped = this.clientStoppedEmitter.event - constructor(installer : LeanInstaller, pkgService : LeanpkgService, outputChannel : OutputChannel) { + constructor(installer: LeanInstaller, outputChannel: OutputChannel) { this.outputChannel = outputChannel; this.installer = installer; - this.pkgService = pkgService; // we must setup the installChanged event handler first before any didOpenEditor calls. installer.installChanged(async (uri: Uri) => await this.onInstallChanged(uri)); // Only change the document language for *visible* documents, // because this closes and then reopens the document. - window.visibleTextEditors.forEach((e) => this.didOpenEditor(e.document)); - this.subscriptions.push(window.onDidChangeVisibleTextEditors((es) => - es.forEach((e) => this.didOpenEditor(e.document)))); + window.visibleTextEditors.forEach(e => this.didOpenEditor(e.document)); + this.subscriptions.push(window.onDidChangeVisibleTextEditors(es => + es.forEach(e => this.didOpenEditor(e.document)))); this.subscriptions.push( commands.registerCommand('lean4.restartFile', () => this.restartFile()), @@ -55,7 +53,7 @@ export class LeanClientProvider implements Disposable { commands.registerCommand('lean4.setup.installElan', () => this.autoInstall()) ); - workspace.onDidOpenTextDocument((document) => this.didOpenEditor(document)); + workspace.onDidOpenTextDocument(document => this.didOpenEditor(document)); workspace.onDidChangeWorkspaceFolders((event) => { for (const folder of event.removed) { @@ -165,8 +163,6 @@ export class LeanClientProvider implements Disposable { } async didOpenEditor(document: TextDocument) { - this.pkgService.didOpen(document.uri); - // bail as quickly as possible on non-lean files. if (document.languageId !== 'lean' && document.languageId !== 'lean4') { return; diff --git a/vscode-lean4/src/utils/errors.ts b/vscode-lean4/src/utils/errors.ts new file mode 100644 index 000000000..5910c4619 --- /dev/null +++ b/vscode-lean4/src/utils/errors.ts @@ -0,0 +1,9 @@ +import { commands, window } from 'vscode'; + +export async function displayErrorWithOutput(message: string) { + const input = 'Show Output' + const choice = await window.showErrorMessage(message, input) + if (choice === input) { + await commands.executeCommand('lean4.troubleshooting.showOutput') + } +} diff --git a/vscode-lean4/src/utils/lake.ts b/vscode-lean4/src/utils/lake.ts index a95651db1..4bf219a51 100644 --- a/vscode-lean4/src/utils/lake.ts +++ b/vscode-lean4/src/utils/lake.ts @@ -15,8 +15,9 @@ export class LakeRunner { this.toolchain = toolchain } - async initProject(name: string, kind: string): Promise { - return this.runLakeCommandSilently('init', [name, kind]) + async initProject(name: string, kind?: string | undefined): Promise { + const args = kind ? [name, kind] : [name] + return this.runLakeCommandSilently('init', args) } async updateDependencies(): Promise { diff --git a/vscode-lean4/src/utils/leanInstaller.ts b/vscode-lean4/src/utils/leanInstaller.ts index ff961b1ad..f73739df0 100644 --- a/vscode-lean4/src/utils/leanInstaller.ts +++ b/vscode-lean4/src/utils/leanInstaller.ts @@ -85,23 +85,25 @@ export class LeanInstaller { } async handleVersionChanged(packageUri : Uri) : Promise { - if (packageUri && packageUri.scheme === 'file'){ + if (packageUri && packageUri.scheme === 'file') { const key = packageUri.fsPath; if (this.versionCache.has(key)) { this.versionCache.delete(key); } } - if (this.promptUser){ - if (this.prompting) { - return; - } - const restartItem = 'Restart Lean'; - const item = await this.showPrompt('Lean version changed', restartItem); - if (item === restartItem) { - await this.checkAndFire(packageUri); - } - } else { + if (!this.promptUser) { + await this.checkAndFire(packageUri); + return + } + + if (this.prompting) { + return; + } + + const restartItem = 'Restart Lean'; + const item = await this.showPrompt('Lean version changed', restartItem); + if (item === restartItem) { await this.checkAndFire(packageUri); } } @@ -126,16 +128,18 @@ export class LeanInstaller { } async handleLakeFileChanged(uri: Uri) : Promise { - if (this.promptUser){ - if (this.prompting) { - return; - } - const restartItem = 'Restart Lean'; - const item = await this.showPrompt('Lake file configuration changed', restartItem); - if (item === restartItem) { - this.installChangedEmitter.fire(uri); - } - } else { + if (!this.promptUser) { + this.installChangedEmitter.fire(uri); + return + } + + if (this.prompting) { + return; + } + + const restartItem = 'Restart Lean'; + const item = await this.showPrompt('Lake file configuration changed', restartItem); + if (item === restartItem) { this.installChangedEmitter.fire(uri); } } @@ -224,7 +228,7 @@ export class LeanInstaller { // looks for a global (default) installation of Lean. This way, we can support // single file editing. logger.log(`executeWithProgress ${cmd} ${options}`) - const checkingResult: ExecutionResult = await batchExecuteWithProgress(cmd, options, 'Checking Lean setup...', { + const checkingResult: ExecutionResult = await batchExecuteWithProgress(cmd, options, 'Checking Lean setup ...', { cwd: folderPath, channel: this.outputChannel }) @@ -302,7 +306,7 @@ export class LeanInstaller { async hasElan() : Promise { try { const options = ['--version'] - const result = await batchExecuteWithProgress('elan', options, 'Checking Elan setup...') + const result = await batchExecuteWithProgress('elan', options, 'Checking Elan setup ...') const filterVersion = /elan (\d+)\.\d+\..+/ const match = filterVersion.exec(result.stdout) return match !== null diff --git a/vscode-lean4/src/utils/leanpkg.ts b/vscode-lean4/src/utils/leanpkg.ts index 3f4020654..91c10ee1c 100644 --- a/vscode-lean4/src/utils/leanpkg.ts +++ b/vscode-lean4/src/utils/leanpkg.ts @@ -24,7 +24,6 @@ export class LeanpkgService implements Disposable { lakeFileChanged = this.lakeFileChangedEmitter.event constructor() { - // track changes in the version of lean specified in the lean-toolchain file // or the leanpkg.toml. While this is looking for all files with these names // it ignores files that are not in the package root. @@ -41,6 +40,11 @@ export class LeanpkgService implements Disposable { watcher2.onDidDelete((u) => this.handleLakeFileChanged(u, true)); this.subscriptions.push(watcher); }); + + window.visibleTextEditors.forEach(e => this.didOpen(e.document.uri)); + this.subscriptions.push(window.onDidChangeVisibleTextEditors(es => + es.forEach(e => this.didOpen(e.document.uri)))); + workspace.onDidOpenTextDocument(document => this.didOpen(document.uri)); } dispose(): void { @@ -49,7 +53,7 @@ export class LeanpkgService implements Disposable { // Must be called when every file is opened so it can track the current contents // of the files we care about. - didOpen(uri: Uri){ + private didOpen(uri: Uri){ const fileName = path.basename(uri.fsPath); if (fileName === this.lakeFileName){ void this.handleLakeFileChanged(uri, false); @@ -66,51 +70,61 @@ export class LeanpkgService implements Disposable { // Note: just opening the file fires this event sometimes which is annoying, so // we compare the contents just to be sure and normalize whitespace so that // just adding a new line doesn't trigger the prompt. - const [workspaceFolder, packageUri, packageFileUri] = await findLeanPackageRoot(uri); - if (packageUri) { - const fileUri = await this.findLakeFile(packageUri); - if (fileUri) { - const contents = await this.readWhitespaceNormalized(fileUri); - let existing : string | undefined; - const key = packageUri.toString(); - if (this.normalizedLakeFileContents.get(key)){ - existing = this.normalizedLakeFileContents.get(key); - } - if (contents !== existing) { - this.normalizedLakeFileContents.set(key, contents); - if (raiseEvent) { - // raise an event so the extension triggers handleLakeFileChanged. - this.lakeFileChangedEmitter.fire(packageUri); - } - } - } + const [_1, packageUri, _2] = await findLeanPackageRoot(uri) + if (!packageUri) { + return + } + + const fileUri = await this.findLakeFile(packageUri) + if (!fileUri) { + return + } + + const contents = await this.readWhitespaceNormalized(fileUri) + let existing : string | undefined + const key = packageUri.toString() + if (this.normalizedLakeFileContents.get(key)) { + existing = this.normalizedLakeFileContents.get(key) + } + if (contents === existing) { + return + } + + this.normalizedLakeFileContents.set(key, contents) + if (raiseEvent) { + // raise an event so the extension triggers handleLakeFileChanged. + this.lakeFileChangedEmitter.fire(packageUri) } } private async handleFileChanged(uri: Uri, raiseEvent : boolean) { // note: apply the same rules here with findLeanPkgVersionInfo no matter // if a file is added or removed so we always match the elan behavior. - const [packageUri, version] = await findLeanPackageVersionInfo(uri); - if (packageUri && version) { - let existing : string | undefined; - const key = packageUri.toString(); - if (this.currentVersion.has(key)){ - existing = this.currentVersion.get(key); - } - if (existing !== version){ - this.currentVersion.set(key, version); - if (raiseEvent) { - // raise an event so the extension triggers handleVersionChanged. - this.versionChangedEmitter.fire(packageUri); - } - } + const [packageUri, version] = await findLeanPackageVersionInfo(uri) + if (!packageUri || !version) { + return + } + + let existing: string | undefined + const key = packageUri.toString() + if (this.currentVersion.has(key)){ + existing = this.currentVersion.get(key) + } + if (existing === version) { + return + } + + this.currentVersion.set(key, version) + if (raiseEvent) { + // raise an event so the extension triggers handleVersionChanged. + this.versionChangedEmitter.fire(packageUri) } } private async findLakeFile(packageUri: Uri) : Promise { const fullPath = Uri.joinPath(packageUri, this.lakeFileName); const url = fullPath.fsPath; - if(await fileExists(url)) { + if (await fileExists(url)) { return fullPath; } return null; diff --git a/vscode-lean4/src/utils/manifest.ts b/vscode-lean4/src/utils/manifest.ts index 1085b7b86..81fb1af46 100644 --- a/vscode-lean4/src/utils/manifest.ts +++ b/vscode-lean4/src/utils/manifest.ts @@ -1,5 +1,7 @@ +import { join } from 'path' import { Uri } from 'vscode' -import { z } from 'zod'; +import { z } from 'zod' +import * as fs from 'fs' export interface DirectGitDependency { name: string @@ -65,3 +67,23 @@ export function parseAsManifest(jsonString: string): Manifest | undefined { return manifest } + +export type ManifestReadError = string + +export async function parseManifestInFolder(folderUri: Uri): Promise { + const manifestPath: string = join(folderUri.fsPath, 'lake-manifest.json') + + let jsonString: string + try { + jsonString = fs.readFileSync(manifestPath, 'utf8') + } catch (e) { + return `Cannot read 'lake-manifest.json' file at ${manifestPath} to determine dependencies.` + } + + const manifest: Manifest | undefined = parseAsManifest(jsonString) + if (!manifest) { + return `Cannot parse 'lake-manifest.json' file at ${manifestPath} to determine dependencies.` + } + + return manifest +} From 137aee79a2634c8aad9a0220a289ede7b67e703e Mon Sep 17 00:00:00 2001 From: mhuisi Date: Tue, 10 Oct 2023 17:55:11 +0200 Subject: [PATCH 10/18] mini --- vscode-lean4/media/guide-help.md | 2 +- vscode-lean4/package.json | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/vscode-lean4/media/guide-help.md b/vscode-lean4/media/guide-help.md index fc8c93927..39212f0e8 100644 --- a/vscode-lean4/media/guide-help.md +++ b/vscode-lean4/media/guide-help.md @@ -5,4 +5,4 @@ To post your question on the [Lean Zulip chat](https://leanprover.zulipchat.com/ 2. [Visit the #new-members stream](https://leanprover.zulipchat.com/#narrow/stream/113489-new-members). 3. Click the 'New topic' button at the bottom of the page, enter a topic title, describe your question or issue in the message text box and click 'Send'. -When posting code on the Lean Zulip chat, please reduce the code to a [minimal working example](https://leanprover-community.github.io/mwe.html) that includes all imports and declarations needed for others to copy and paste the code into their own development environment. +When posting code on the Lean Zulip chat, please reduce the code to a [minimal working example](https://leanprover-community.github.io/mwe.html) that includes all imports and declarations needed for others to copy and paste the code into their own development environment. Please also make sure to delimit the code by [three backticks](https://github.com/leanprover-community/mathlib/wiki/Code-in-backticks) so that the code is highlighted and formatted correctly. diff --git a/vscode-lean4/package.json b/vscode-lean4/package.json index e14b98dbe..643be69eb 100644 --- a/vscode-lean4/package.json +++ b/vscode-lean4/package.json @@ -710,11 +710,6 @@ "editor.wordSeparators": "`~@$%^&*()-=+[{]}⟨⟩⦃⦄⟦⟧⟮⟯‹›\\|;:\",.<>/" } }, - "taskDefinitions": [ - { - "type": "lean4" - } - ], "walkthroughs": [ { "id": "guide.linux", @@ -865,7 +860,6 @@ "activationEvents": [ "onLanguage:lean", "onLanguage:markdown", - "onCommand:workbench.action.tasks.runTask", "onStartupFinished" ], "main": "./dist/extension", From f129190f74a354bb56945d7dac1154e028768ce7 Mon Sep 17 00:00:00 2001 From: mhuisi Date: Wed, 11 Oct 2023 16:07:09 +0200 Subject: [PATCH 11/18] minor changes - lazily activate lean 4 features correctly - progress titles - active folder errors - no progress for version checks (too noisy) --- vscode-lean4/media/guide-documentation.md | 2 +- vscode-lean4/src/extension.ts | 6 +-- vscode-lean4/src/leanclient.ts | 7 ++- vscode-lean4/src/projectinit.ts | 6 +-- vscode-lean4/src/projectoperations.ts | 2 + vscode-lean4/src/utils/batch.ts | 9 ++-- vscode-lean4/src/utils/clientProvider.ts | 6 +-- vscode-lean4/src/utils/elan.ts | 2 +- vscode-lean4/src/utils/lake.ts | 13 +++--- vscode-lean4/src/utils/leanInstaller.ts | 7 +-- vscode-lean4/src/utils/projectInfo.ts | 57 ++++++++++------------- 11 files changed, 52 insertions(+), 65 deletions(-) diff --git a/vscode-lean4/media/guide-documentation.md b/vscode-lean4/media/guide-documentation.md index e5fb82e98..690c16cc6 100644 --- a/vscode-lean4/media/guide-documentation.md +++ b/vscode-lean4/media/guide-documentation.md @@ -1,5 +1,5 @@ ## Books -If you want to learn Lean 4, choose one of the following introductory books based on your background. If you are getting stuck or have any questions, click on the 'Questions and Troubleshooting' step below. +If you want to learn Lean 4, choose one of the following introductory books based on your background. If you are getting stuck or have any questions, click on the 'Questions and Troubleshooting' step at the bottom on the left side. - [Functional Programming in Lean](https://lean-lang.org/functional_programming_in_lean/) The standard introduction for using Lean 4 as a general-purpose programming language. diff --git a/vscode-lean4/src/extension.ts b/vscode-lean4/src/extension.ts index 738bb9a01..59113a12e 100644 --- a/vscode-lean4/src/extension.ts +++ b/vscode-lean4/src/extension.ts @@ -1,4 +1,4 @@ -import { window, ExtensionContext, TextDocument, tasks, commands, Disposable } from 'vscode' +import { window, ExtensionContext, TextDocument, tasks, commands, Disposable, workspace } from 'vscode' import { AbbreviationFeature } from './abbreviation' import { InfoProvider } from './infoview' import { DocViewProvider } from './docview' @@ -177,8 +177,8 @@ export async function activate(context: ExtensionContext): Promise { } // No Lean 4 document yet => Load remaining features when one is open - const disposeActivationListener: Disposable = window.onDidChangeVisibleTextEditors(_ => { - if (findOpenLeanDocument()) { + const disposeActivationListener: Disposable = workspace.onDidOpenTextDocument(doc => { + if (isLean(doc.languageId)) { activateLean4Features(context, alwaysEnabledFeatures.installer) disposeActivationListener.dispose() } diff --git a/vscode-lean4/src/leanclient.ts b/vscode-lean4/src/leanclient.ts index ee2f5d52f..888194058 100644 --- a/vscode-lean4/src/leanclient.ts +++ b/vscode-lean4/src/leanclient.ts @@ -150,7 +150,7 @@ export class LeanClient implements Disposable { const progressOptions: ProgressOptions = { location: ProgressLocation.Notification, - title: '', + title: 'Starting Lean language client', cancellable: false } await window.withProgress(progressOptions, async progress => @@ -164,8 +164,7 @@ export class LeanClient implements Disposable { // Should only be called from `restart` const startTime = Date.now() - const message = 'Starting Lean language client ...' - progress.report({ message, increment: 0 }) + progress.report({ increment: 0 }) this.client = await this.setupClient() let insideRestart = true; @@ -192,7 +191,7 @@ export class LeanClient implements Disposable { } } }) - progress.report({ message, increment: 80 }) + progress.report({ increment: 80 }) await this.client.start() // tell the new client about the documents that are already open! for (const key of this.isOpen.keys()) { diff --git a/vscode-lean4/src/projectinit.ts b/vscode-lean4/src/projectinit.ts index 526aee48e..8d02c4fc5 100644 --- a/vscode-lean4/src/projectinit.ts +++ b/vscode-lean4/src/projectinit.ts @@ -84,7 +84,7 @@ export class ProjectInitializationProvider implements Disposable { private async openProject() { const projectFolders: Uri[] | undefined = await window.showOpenDialog({ - title: 'Open Lean 4 project folder containing a `lean-toolchain` file', + title: 'Open Lean 4 project folder containing a \'lean-toolchain\' file', openLabel: 'Open project folder', canSelectFiles: false, canSelectFolders: true, @@ -119,7 +119,7 @@ export class ProjectInitializationProvider implements Disposable { } const message = `The selected folder is not a valid Lean 4 project folder because it does not contain a 'lean-toolchain' file. -However, a valid Lean 4 project folder was found in one of the parent directories at ${parentProjectFolder.fsPath}. +However, a valid Lean 4 project folder was found in one of the parent directories at '${parentProjectFolder.fsPath}'. Open this project instead?` const input = 'Open parent directory project' const choice: string | undefined = await window.showInformationMessage(message, { modal: true }, input) @@ -157,7 +157,7 @@ Open this project instead?` return } - const result: ExecutionResult = await batchExecuteWithProgress('git', ['clone', existingProjectUri.toString(), projectFolder.fsPath], 'Cloning project ...', { channel: this.channel, allowCancellation: true }) + const result: ExecutionResult = await batchExecuteWithProgress('git', ['clone', existingProjectUri.toString(), projectFolder.fsPath], 'Cloning project', { channel: this.channel, allowCancellation: true }) if (result.exitCode === ExecutionExitCode.Cancelled) { return } diff --git a/vscode-lean4/src/projectoperations.ts b/vscode-lean4/src/projectoperations.ts index 63df86f34..43d347fa9 100644 --- a/vscode-lean4/src/projectoperations.ts +++ b/vscode-lean4/src/projectoperations.ts @@ -114,6 +114,7 @@ export class ProjectOperationProvider implements Disposable { const [_1, folderUri, _2] = await findLeanPackageRoot(window.activeTextEditor.document.uri) if (!folderUri) { + void window.showErrorMessage('Cannot determine active project from currently open file.') return } @@ -315,6 +316,7 @@ export class ProjectOperationProvider implements Disposable { const lakeRunner: LakeRunner | 'NoActiveFolder' = await lakeInActiveFolder(this.channel) if (lakeRunner === 'NoActiveFolder') { + void window.showErrorMessage('Cannot determine active project from currently open file.') this.isRunningOperation = false return } diff --git a/vscode-lean4/src/utils/batch.ts b/vscode-lean4/src/utils/batch.ts index 3fecfcd94..d66dae773 100644 --- a/vscode-lean4/src/utils/batch.ts +++ b/vscode-lean4/src/utils/batch.ts @@ -137,14 +137,14 @@ interface ProgressExecutionOptions { export async function batchExecuteWithProgress( executablePath: string, args: string[], - prompt: string, + title: string, options: ProgressExecutionOptions = {}): Promise { - const messagePrefix = options.channel ? '[(Details)](command:lean4.troubleshooting.showOutput) ' : '' + const titleSuffix = options.channel ? ' [(Details)](command:lean4.troubleshooting.showOutput)' : '' const progressOptions: ProgressOptions = { location: ProgressLocation.Notification, - title: '', + title: title + titleSuffix, cancellable: options.allowCancellation === true } let inc = 0 @@ -166,7 +166,7 @@ export async function batchExecuteWithProgress( if (inc < 90) { inc += 2 } - progress.report({ increment: inc, message: messagePrefix + value }) + progress.report({ increment: inc, message: value }) }, appendLine(value: string) { this.append(value + '\n') @@ -177,7 +177,6 @@ export async function batchExecuteWithProgress( hide() { /* empty */ }, dispose() { /* empty */ } } - progress.report({ increment: 0, message: prompt }); return batchExecute(executablePath, args, options.cwd, { combined: progressChannel }, token); }); return result; diff --git a/vscode-lean4/src/utils/clientProvider.ts b/vscode-lean4/src/utils/clientProvider.ts index 9e60454f3..7eaad73ba 100644 --- a/vscode-lean4/src/utils/clientProvider.ts +++ b/vscode-lean4/src/utils/clientProvider.ts @@ -91,7 +91,7 @@ export class LeanClientProvider implements Disposable { if (uri) { // have to check again here in case elan install had --default-toolchain none. const [workspaceFolder, folder, packageFileUri] = await findLeanPackageRoot(uri); - const packageUri = folder ? folder : Uri.from({scheme: 'untitled'}); + const packageUri = folder ?? Uri.from({scheme: 'untitled'}); logger.log('[ClientProvider] testLeanVersion'); const version = await this.installer.testLeanVersion(packageUri); if (version.version === '4') { @@ -286,7 +286,7 @@ export class LeanClientProvider implements Disposable { // Returns a null client if it turns out the new workspace is a lean3 workspace. async ensureClient(uri : Uri, versionInfo: LeanVersion | undefined) : Promise<[boolean,LeanClient | undefined]> { const [workspaceFolder, folder, packageFileUri] = await findLeanPackageRoot(uri); - const folderUri = folder ? folder : Uri.from({scheme: 'untitled'}); + const folderUri = folder ?? Uri.from({scheme: 'untitled'}); let client = this.getClientForFolder(folderUri); const key = this.getKeyFromUri(folder); const cachedClient = (client !== undefined); @@ -388,7 +388,7 @@ export class LeanClientProvider implements Disposable { } const message = `Opened folder is not a valid Lean 4 project folder because it does not contain a 'lean-toolchain' file. -However, a valid Lean 4 project folder was found in one of the parent directories at ${parentProjectFolder.fsPath}. +However, a valid Lean 4 project folder was found in one of the parent directories at '${parentProjectFolder.fsPath}'. Open this project instead?` const input = 'Open parent directory project' const choice: string | undefined = await window.showWarningMessage(message, input) diff --git a/vscode-lean4/src/utils/elan.ts b/vscode-lean4/src/utils/elan.ts index e79218692..d707686c1 100644 --- a/vscode-lean4/src/utils/elan.ts +++ b/vscode-lean4/src/utils/elan.ts @@ -2,5 +2,5 @@ import { OutputChannel } from 'vscode'; import { ExecutionResult, batchExecuteWithProgress } from './batch'; export async function elanSelfUpdate(channel: OutputChannel): Promise { - return await batchExecuteWithProgress('elan', ['self', 'update'], 'Updating Elan ...', { channel }) + return await batchExecuteWithProgress('elan', ['self', 'update'], 'Updating Elan', { channel }) } diff --git a/vscode-lean4/src/utils/lake.ts b/vscode-lean4/src/utils/lake.ts index 4bf219a51..32557c6b4 100644 --- a/vscode-lean4/src/utils/lake.ts +++ b/vscode-lean4/src/utils/lake.ts @@ -21,23 +21,23 @@ export class LakeRunner { } async updateDependencies(): Promise { - return this.runLakeCommandWithProgress('update', [], 'Updating dependencies ...') + return this.runLakeCommandWithProgress('update', [], 'Updating dependencies') } async updateDependency(dependencyName: string): Promise { - return this.runLakeCommandWithProgress('update', [dependencyName], `Updating '${dependencyName}' dependency ...`) + return this.runLakeCommandWithProgress('update', [dependencyName], `Updating '${dependencyName}' dependency`) } async build(): Promise { - return this.runLakeCommandWithProgress('build', [], 'Building Lean project ...') + return this.runLakeCommandWithProgress('build', [], 'Building Lean project') } async clean(): Promise { - return this.runLakeCommandWithProgress('clean', [], 'Cleaning Lean project ...') + return this.runLakeCommandWithProgress('clean', [], 'Cleaning Lean project') } async fetchMathlibCache(filterError: boolean = false): Promise { - const prompt = 'Checking whether Mathlib build artifact cache needs to be downloaded ...' + const prompt = 'Checking Mathlib build artifact cache' return this.runLakeCommandWithProgress('exe', ['cache', 'get'], prompt, line => { if (filterError && line.includes(cacheNotFoundError)) { return undefined @@ -47,7 +47,7 @@ export class LakeRunner { } async isMathlibCacheGetAvailable(): Promise<'Yes' | 'No' | 'Cancelled'> { - const result: ExecutionResult = await this.runLakeCommandWithProgress('exe', ['cache'], 'Checking whether this is a Mathlib project ...') + const result: ExecutionResult = await this.runLakeCommandWithProgress('exe', ['cache'], 'Checking whether this is a Mathlib project') if (result.exitCode === ExecutionExitCode.Cancelled) { return 'Cancelled' } @@ -93,7 +93,6 @@ export async function lakeInActiveFolder(channel: OutputChannel, toolchain?: str if (!window.activeTextEditor) { return 'NoActiveFolder' } - const [_1, folderUri, _2] = await findLeanPackageRoot(window.activeTextEditor.document.uri) if (!folderUri) { return 'NoActiveFolder' diff --git a/vscode-lean4/src/utils/leanInstaller.ts b/vscode-lean4/src/utils/leanInstaller.ts index f73739df0..b052105ae 100644 --- a/vscode-lean4/src/utils/leanInstaller.ts +++ b/vscode-lean4/src/utils/leanInstaller.ts @@ -228,10 +228,7 @@ export class LeanInstaller { // looks for a global (default) installation of Lean. This way, we can support // single file editing. logger.log(`executeWithProgress ${cmd} ${options}`) - const checkingResult: ExecutionResult = await batchExecuteWithProgress(cmd, options, 'Checking Lean setup ...', { - cwd: folderPath, - channel: this.outputChannel - }) + const checkingResult: ExecutionResult = await batchExecute(cmd, options, folderPath, { combined: this.outputChannel }) if (checkingResult.exitCode === ExecutionExitCode.CannotLaunch) { result.error = 'lean not found' } else if (checkingResult.stderr.indexOf('no default toolchain') > 0) { @@ -306,7 +303,7 @@ export class LeanInstaller { async hasElan() : Promise { try { const options = ['--version'] - const result = await batchExecuteWithProgress('elan', options, 'Checking Elan setup ...') + const result = await batchExecute('elan', options) const filterVersion = /elan (\d+)\.\d+\..+/ const match = filterVersion.exec(result.stdout) return match !== null diff --git a/vscode-lean4/src/utils/projectInfo.ts b/vscode-lean4/src/utils/projectInfo.ts index 272a1ad6d..91ae23b74 100644 --- a/vscode-lean4/src/utils/projectInfo.ts +++ b/vscode-lean4/src/utils/projectInfo.ts @@ -23,14 +23,11 @@ export async function isCoreLean4Directory(path: Uri): Promise { // Find the root of a Lean project and return an optional WorkspaceFolder for it, // the Uri for the package root and the Uri for the 'leanpkg.toml' or 'lean-toolchain' file found there. export async function findLeanPackageRoot(uri: Uri) : Promise<[WorkspaceFolder | undefined, Uri | null, Uri | null]> { - if (!uri) return [undefined, null, null]; + if (!uri || uri.scheme !== 'file') return [undefined, null, null]; const toolchainFileName = 'lean-toolchain'; const tomlFileName = 'leanpkg.toml'; - if (uri.scheme === 'untitled'){ - // then return a Uri representing all untitled files. - return [undefined, Uri.from({scheme: 'untitled'}), null]; - } + let path = uri; let wsFolder = workspace.getWorkspaceFolder(uri); if (!wsFolder && workspace.workspaceFolders) { @@ -44,7 +41,7 @@ export async function findLeanPackageRoot(uri: Uri) : Promise<[WorkspaceFolder | if (wsFolder){ // jump to the real workspace folder if we have a Workspace for this file. path = wsFolder.uri; - } else if (path.scheme === 'file') { + } else { // then start searching from the directory containing this document. // The given uri may already be a folder Uri in some cases. if (fs.lstatSync(path.fsPath).isFile()) { @@ -54,34 +51,28 @@ export async function findLeanPackageRoot(uri: Uri) : Promise<[WorkspaceFolder | } const startFolder = path; - if (path.scheme === 'file') { - // search parent folders for a leanpkg.toml file, or a Lake lean-toolchain file. - while (true) { - // give preference to 'lean-toolchain' files if any. - const leanToolchain = Uri.joinPath(path, toolchainFileName); - if (await fileExists(leanToolchain.fsPath)) { - return [wsFolder, path, leanToolchain]; - } - else { - const leanPkg = Uri.joinPath(path, tomlFileName); - if (await fileExists(leanPkg.fsPath)) { - return [wsFolder, path, leanPkg]; - } - else if (await isCoreLean4Directory(path)) { - return [wsFolder, path, null]; - } - else if (searchUpwards) { - const parent = Uri.joinPath(path, '..'); - if (parent === path) { - // no project file found. - break; - } - path = parent; - } - else { - // don't search above a WorkspaceFolder barrier. + // search parent folders for a leanpkg.toml file, or a Lake lean-toolchain file. + while (true) { + // give preference to 'lean-toolchain' files if any. + const leanToolchain = Uri.joinPath(path, toolchainFileName); + if (await fileExists(leanToolchain.fsPath)) { + return [wsFolder, path, leanToolchain]; + } else { + const leanPkg = Uri.joinPath(path, tomlFileName); + if (await fileExists(leanPkg.fsPath)) { + return [wsFolder, path, leanPkg]; + } else if (await isCoreLean4Directory(path)) { + return [wsFolder, path, null]; + } else if (searchUpwards) { + const parent = Uri.joinPath(path, '..'); + if (parent === path) { + // no project file found. break; } + path = parent; + } else { + // don't search above a WorkspaceFolder barrier. + break; } } } @@ -95,7 +86,7 @@ export async function findLeanPackageRoot(uri: Uri) : Promise<[WorkspaceFolder | export async function findLeanPackageVersionInfo(uri: Uri) : Promise<[Uri | null, string | null]> { const [_, packageUri, packageFileUri] = await findLeanPackageRoot(uri); - if (!packageUri || packageUri.scheme === 'untitled') return [null, null]; + if (!packageUri) return [null, null]; let version : string | null = null; if (packageFileUri) { From c3770deb9f4144672fa97808e407fea741ee5020 Mon Sep 17 00:00:00 2001 From: mhuisi Date: Thu, 12 Oct 2023 09:35:13 +0200 Subject: [PATCH 12/18] always run lake update after init & init progress --- vscode-lean4/src/projectinit.ts | 19 +++++++++---------- vscode-lean4/src/utils/batch.ts | 1 + vscode-lean4/src/utils/lake.ts | 3 ++- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/vscode-lean4/src/projectinit.ts b/vscode-lean4/src/projectinit.ts index 8d02c4fc5..c7c95d580 100644 --- a/vscode-lean4/src/projectinit.ts +++ b/vscode-lean4/src/projectinit.ts @@ -29,20 +29,10 @@ export class ProjectInitializationProvider implements Disposable { private async createMathlibProject() { const mathlibToolchain = 'leanprover-community/mathlib4:lean-toolchain' const projectFolder: Uri | 'DidNotComplete' = await this.createProject('math', mathlibToolchain) - if (projectFolder === 'DidNotComplete') { return } - const updateResult: ExecutionResult = await lake(this.channel, projectFolder, mathlibToolchain).updateDependencies() - if (updateResult.exitCode === ExecutionExitCode.Cancelled) { - return - } - if (updateResult.exitCode !== ExecutionExitCode.Success) { - await displayError(updateResult, 'Cannot update dependencies.') - return - } - const cacheGetResult: ExecutionResult = await lake(this.channel, projectFolder, mathlibToolchain).fetchMathlibCache() if (cacheGetResult.exitCode === ExecutionExitCode.Cancelled) { return @@ -79,6 +69,15 @@ export class ProjectInitializationProvider implements Disposable { return 'DidNotComplete' } + const updateResult: ExecutionResult = await lake(this.channel, projectFolder, toolchain).updateDependencies() + if (updateResult.exitCode === ExecutionExitCode.Cancelled) { + return 'DidNotComplete' + } + if (updateResult.exitCode !== ExecutionExitCode.Success) { + await displayError(updateResult, 'Cannot update dependencies.') + return 'DidNotComplete' + } + return projectFolder } diff --git a/vscode-lean4/src/utils/batch.ts b/vscode-lean4/src/utils/batch.ts index d66dae773..f1559b653 100644 --- a/vscode-lean4/src/utils/batch.ts +++ b/vscode-lean4/src/utils/batch.ts @@ -177,6 +177,7 @@ export async function batchExecuteWithProgress( hide() { /* empty */ }, dispose() { /* empty */ } } + progress.report({ increment: 0 }) return batchExecute(executablePath, args, options.cwd, { combined: progressChannel }, token); }); return result; diff --git a/vscode-lean4/src/utils/lake.ts b/vscode-lean4/src/utils/lake.ts index 32557c6b4..badd010aa 100644 --- a/vscode-lean4/src/utils/lake.ts +++ b/vscode-lean4/src/utils/lake.ts @@ -3,6 +3,7 @@ import { ExecutionExitCode, ExecutionResult, batchExecute, batchExecuteWithProgr import { findLeanPackageRoot } from './projectInfo'; export const cacheNotFoundError = 'unknown executable `cache`' +export const cacheNotFoundExitError = '=> Operation failed. Exit Code: 1.' export class LakeRunner { channel: OutputChannel @@ -17,7 +18,7 @@ export class LakeRunner { async initProject(name: string, kind?: string | undefined): Promise { const args = kind ? [name, kind] : [name] - return this.runLakeCommandSilently('init', args) + return this.runLakeCommandWithProgress('init', args, 'Initializing project') } async updateDependencies(): Promise { From 2d1aefc62001816db6983a0f7719a6109071214c Mon Sep 17 00:00:00 2001 From: mhuisi Date: Fri, 13 Oct 2023 14:46:44 +0200 Subject: [PATCH 13/18] use leanclientprovider for determining active folder --- vscode-lean4/src/projectoperations.ts | 22 +++++++++------------- vscode-lean4/src/utils/lake.ts | 15 +-------------- 2 files changed, 10 insertions(+), 27 deletions(-) diff --git a/vscode-lean4/src/projectoperations.ts b/vscode-lean4/src/projectoperations.ts index 43d347fa9..443f6074e 100644 --- a/vscode-lean4/src/projectoperations.ts +++ b/vscode-lean4/src/projectoperations.ts @@ -1,5 +1,5 @@ import { Disposable, commands, window, OutputChannel, QuickPickItem, Uri } from 'vscode'; -import { LakeRunner, cacheNotFoundError, lakeInActiveFolder } from './utils/lake'; +import { LakeRunner, cacheNotFoundError, lake, lakeInActiveFolder } from './utils/lake'; import { ExecutionExitCode, ExecutionResult, batchExecute, displayError } from './utils/batch'; import { LeanClientProvider } from './utils/clientProvider'; import { LeanClient } from './leanclient'; @@ -112,13 +112,14 @@ export class ProjectOperationProvider implements Disposable { return } - const [_1, folderUri, _2] = await findLeanPackageRoot(window.activeTextEditor.document.uri) - if (!folderUri) { - void window.showErrorMessage('Cannot determine active project from currently open file.') + const activeClient: LeanClient | undefined = this.clientProvider.getActiveClient() + if (!activeClient) { + void window.showErrorMessage('No active client.') + this.isRunningOperation = false return } - const manifestResult: Manifest | ManifestReadError = await parseManifestInFolder(folderUri) + const manifestResult: Manifest | ManifestReadError = await parseManifestInFolder(activeClient.folderUri) if (typeof manifestResult === 'string') { void window.showErrorMessage(manifestResult) return @@ -155,7 +156,7 @@ export class ProjectOperationProvider implements Disposable { return } - const localToolchainPath: string = join(folderUri.fsPath, 'lean-toolchain') + const localToolchainPath: string = join(activeClient.folderUri.fsPath, 'lean-toolchain') const dependencyToolchainResult: string | 'DoNotUpdate' | 'Cancelled' = await this.determineDependencyToolchain(localToolchainPath, dependencyChoice) if (dependencyToolchainResult === 'Cancelled') { return @@ -314,13 +315,6 @@ export class ProjectOperationProvider implements Disposable { return } - const lakeRunner: LakeRunner | 'NoActiveFolder' = await lakeInActiveFolder(this.channel) - if (lakeRunner === 'NoActiveFolder') { - void window.showErrorMessage('Cannot determine active project from currently open file.') - this.isRunningOperation = false - return - } - const activeClient: LeanClient | undefined = this.clientProvider.getActiveClient() if (!activeClient) { void window.showErrorMessage('No active client.') @@ -328,6 +322,8 @@ export class ProjectOperationProvider implements Disposable { return } + const lakeRunner: LakeRunner = lake(this.channel, activeClient.folderUri) + const result: 'Success' | 'IsRestarting' = await activeClient.withStoppedClient(() => command(lakeRunner)) if (result === 'IsRestarting') { void window.showErrorMessage('Cannot run project action while restarting the server.') diff --git a/vscode-lean4/src/utils/lake.ts b/vscode-lean4/src/utils/lake.ts index badd010aa..bc7b91c53 100644 --- a/vscode-lean4/src/utils/lake.ts +++ b/vscode-lean4/src/utils/lake.ts @@ -1,6 +1,5 @@ -import { OutputChannel, Uri, window } from 'vscode'; +import { OutputChannel, Uri } from 'vscode'; import { ExecutionExitCode, ExecutionResult, batchExecute, batchExecuteWithProgress } from './batch'; -import { findLeanPackageRoot } from './projectInfo'; export const cacheNotFoundError = 'unknown executable `cache`' export const cacheNotFoundExitError = '=> Operation failed. Exit Code: 1.' @@ -89,15 +88,3 @@ export class LakeRunner { export function lake(channel: OutputChannel, cwdUri: Uri | undefined, toolchain?: string | undefined): LakeRunner { return new LakeRunner(channel, cwdUri, toolchain) } - -export async function lakeInActiveFolder(channel: OutputChannel, toolchain?: string | undefined): Promise { - if (!window.activeTextEditor) { - return 'NoActiveFolder' - } - const [_1, folderUri, _2] = await findLeanPackageRoot(window.activeTextEditor.document.uri) - if (!folderUri) { - return 'NoActiveFolder' - } - - return lake(channel, folderUri, toolchain) -} From 0455c7d59affbc973562c4bed05ed78fdd733b98 Mon Sep 17 00:00:00 2001 From: mhuisi Date: Fri, 13 Oct 2023 15:03:59 +0200 Subject: [PATCH 14/18] chore: dead code --- vscode-lean4/src/projectoperations.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vscode-lean4/src/projectoperations.ts b/vscode-lean4/src/projectoperations.ts index 443f6074e..68c40cb73 100644 --- a/vscode-lean4/src/projectoperations.ts +++ b/vscode-lean4/src/projectoperations.ts @@ -1,9 +1,8 @@ import { Disposable, commands, window, OutputChannel, QuickPickItem, Uri } from 'vscode'; -import { LakeRunner, cacheNotFoundError, lake, lakeInActiveFolder } from './utils/lake'; +import { LakeRunner, cacheNotFoundError, lake } from './utils/lake'; import { ExecutionExitCode, ExecutionResult, batchExecute, displayError } from './utils/batch'; import { LeanClientProvider } from './utils/clientProvider'; import { LeanClient } from './leanclient'; -import { findLeanPackageRoot } from './utils/projectInfo'; import { join } from 'path'; import * as fs from 'fs' import { DirectGitDependency, Manifest, ManifestReadError, parseAsManifest, parseManifestInFolder } from './utils/manifest'; From e963fa78fe4a92e23521dcd4f0b0a04fe4271a6c Mon Sep 17 00:00:00 2001 From: mhuisi Date: Fri, 13 Oct 2023 15:45:08 +0200 Subject: [PATCH 15/18] make sure that install elan command works pre-lean4-activation --- vscode-lean4/src/extension.ts | 11 ++++++++++- vscode-lean4/src/utils/clientProvider.ts | 3 +-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/vscode-lean4/src/extension.ts b/vscode-lean4/src/extension.ts index 59113a12e..b4d8745bf 100644 --- a/vscode-lean4/src/extension.ts +++ b/vscode-lean4/src/extension.ts @@ -6,7 +6,7 @@ import { LeanTaskGutter } from './taskgutter' import { LeanInstaller } from './utils/leanInstaller' import { LeanpkgService } from './utils/leanpkg' import { LeanClientProvider } from './utils/clientProvider' -import { addDefaultElanPath, removeElanPath, addToolchainBinPath, isElanDisabled, getDefaultLeanVersion} from './config' +import { addDefaultElanPath, removeElanPath, addToolchainBinPath, isElanDisabled, getDefaultLeanVersion, getDefaultElanPath} from './config' import { findLeanPackageVersionInfo } from './utils/projectInfo' import { Exports } from './exports'; import { logger } from './utils/logger' @@ -85,6 +85,15 @@ function activateAlwaysEnabledFeatures(context: ExtensionContext): AlwaysEnabled const defaultToolchain = getDefaultLeanVersion(); const installer = new LeanInstaller(outputChannel, defaultToolchain) + context.subscriptions.push(commands.registerCommand('lean4.setup.installElan', async () => { + await installer.installElan(); + if (isElanDisabled()) { + addToolchainBinPath(getDefaultElanPath()); + } else { + addDefaultElanPath(); + } + })) + return { docView, projectInitializationProvider, installer } } diff --git a/vscode-lean4/src/utils/clientProvider.ts b/vscode-lean4/src/utils/clientProvider.ts index 7eaad73ba..6f3e3ed23 100644 --- a/vscode-lean4/src/utils/clientProvider.ts +++ b/vscode-lean4/src/utils/clientProvider.ts @@ -49,8 +49,7 @@ export class LeanClientProvider implements Disposable { commands.registerCommand('lean4.restartFile', () => this.restartFile()), commands.registerCommand('lean4.refreshFileDependencies', () => this.restartFile()), commands.registerCommand('lean4.restartServer', () => this.restartActiveClient()), - commands.registerCommand('lean4.stopServer', () => this.stopActiveClient()), - commands.registerCommand('lean4.setup.installElan', () => this.autoInstall()) + commands.registerCommand('lean4.stopServer', () => this.stopActiveClient()) ); workspace.onDidOpenTextDocument(document => this.didOpenEditor(document)); From 2cd58e8d8c80ae1f1e576f3f0aaaf9dc063c7a42 Mon Sep 17 00:00:00 2001 From: mhuisi Date: Tue, 17 Oct 2023 16:07:06 +0200 Subject: [PATCH 16/18] incorporate patrick's suggestions --- vscode-lean4/package.json | 44 ++++++++++++------------ vscode-lean4/src/extension.ts | 19 ++++++---- vscode-lean4/src/infoview.ts | 8 +++-- vscode-lean4/src/projectoperations.ts | 12 +++---- vscode-lean4/src/utils/clientProvider.ts | 12 +++++-- 5 files changed, 56 insertions(+), 39 deletions(-) diff --git a/vscode-lean4/package.json b/vscode-lean4/package.json index 8b5d85865..87bfcc034 100644 --- a/vscode-lean4/package.json +++ b/vscode-lean4/package.json @@ -540,124 +540,124 @@ "editor/title": [ { "submenu": "lean4.titlebar", - "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", + "when": "config.lean4.alwaysShowTitleBarMenu || lean4.isLeanFeatureSetActive", "group": "navigation@0" } ], "lean4.titlebar": [ { "submenu": "lean4.titlebar.newProject", - "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", + "when": "config.lean4.alwaysShowTitleBarMenu || lean4.isLeanFeatureSetActive", "group": "1_setup@1" }, { "submenu": "lean4.titlebar.openProject", - "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", + "when": "config.lean4.alwaysShowTitleBarMenu || lean4.isLeanFeatureSetActive", "group": "1_setup@1" }, { "command": "lean4.restartFile", - "when": "editorLangId == lean4", + "when": "lean4.isLeanFeatureSetActive", "group": "2_server@1" }, { "command": "lean4.restartServer", - "when": "editorLangId == lean4", + "when": "lean4.isLeanFeatureSetActive", "group": "2_server@2" }, { "command": "lean4.toggleInfoview", - "when": "editorLangId == lean4", + "when": "lean4.isLeanFeatureSetActive", "group": "3_infoview@1" }, { "command": "lean4.troubleshooting.showOutput", - "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", + "when": "config.lean4.alwaysShowTitleBarMenu || lean4.isLeanFeatureSetActive", "group": "4_troubleshooting" }, { "submenu": "lean4.titlebar.versions", - "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", + "when": "config.lean4.alwaysShowTitleBarMenu || lean4.isLeanFeatureSetActive", "group": "5_versions" }, { "submenu": "lean4.titlebar.projectActions", - "when": "editorLangId == lean4", + "when": "config.lean4.alwaysShowTitleBarMenu || lean4.isLeanFeatureSetActive", "group": "6_projectActions@1" }, { "submenu": "lean4.titlebar.documentation", - "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", + "when": "config.lean4.alwaysShowTitleBarMenu || lean4.isLeanFeatureSetActive", "group": "7_documentation@1" } ], "lean4.titlebar.newProject": [ { "command": "lean4.project.createStandaloneProject", - "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", + "when": "config.lean4.alwaysShowTitleBarMenu || lean4.isLeanFeatureSetActive", "group": "1_newProject@1" }, { "command": "lean4.project.createMathlibProject", - "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", + "when": "config.lean4.alwaysShowTitleBarMenu || lean4.isLeanFeatureSetActive", "group": "1_newProject@2" } ], "lean4.titlebar.openProject": [ { "command": "lean4.project.open", - "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", + "when": "config.lean4.alwaysShowTitleBarMenu || lean4.isLeanFeatureSetActive", "group": "1_openProject@1" }, { "command": "lean4.project.clone", - "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", + "when": "config.lean4.alwaysShowTitleBarMenu || lean4.isLeanFeatureSetActive", "group": "1_openProject@2" } ], "lean4.titlebar.versions": [ { "command": "lean4.setup.installElan", - "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", + "when": "config.lean4.alwaysShowTitleBarMenu || lean4.isLeanFeatureSetActive", "group": "1_setup@1" } ], "lean4.titlebar.projectActions": [ { "command": "lean4.project.build", - "when": "editorLangId == lean4", + "when": "lean4.isLeanFeatureSetActive", "group": "1_projectActions@1" }, { "command": "lean4.project.clean", - "when": "editorLangId == lean4", + "when": "lean4.isLeanFeatureSetActive", "group": "1_projectActions@2" }, { "command": "lean4.project.updateDependency", - "when": "editorLangId == lean4", + "when": "lean4.isLeanFeatureSetActive", "group": "1_projectActions@3" }, { "command": "lean4.project.fetchCache", - "when": "editorLangId == lean4", + "when": "lean4.isLeanFeatureSetActive", "group": "2_mathlibActions@1" } ], "lean4.titlebar.documentation": [ { "command": "lean4.setup.showSetupGuide", - "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", + "when": "config.lean4.alwaysShowTitleBarMenu || lean4.isLeanFeatureSetActive", "group": "1_installation@1" }, { "command": "lean4.docView.open", - "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", + "when": "config.lean4.alwaysShowTitleBarMenu || lean4.isLeanFeatureSetActive", "group": "2_docview@1" }, { "command": "lean4.docView.showAllAbbreviations", - "when": "config.lean4.alwaysShowTitleBarMenu || editorLangId == lean4", + "when": "config.lean4.alwaysShowTitleBarMenu || lean4.isLeanFeatureSetActive", "group": "2_docview@2" } ], diff --git a/vscode-lean4/src/extension.ts b/vscode-lean4/src/extension.ts index b4d8745bf..e254000de 100644 --- a/vscode-lean4/src/extension.ts +++ b/vscode-lean4/src/extension.ts @@ -26,6 +26,10 @@ interface Lean4EnabledFeatures { projectOperationProvider: ProjectOperationProvider } +async function setLeanFeatureSetActive(isActive: boolean) { + await commands.executeCommand('setContext', 'lean4.isLeanFeatureSetActive', isActive) +} + function isLean(languageId : string) : boolean { return languageId === 'lean' || languageId === 'lean4'; } @@ -125,7 +129,7 @@ function activateAbbreviationFeature(context: ExtensionContext, docView: DocView return abbrev } -function activateLean4Features(context: ExtensionContext, installer: LeanInstaller): Lean4EnabledFeatures { +async function activateLean4Features(context: ExtensionContext, installer: LeanInstaller): Promise { const clientProvider = new LeanClientProvider(installer, installer.getOutputChannel()); context.subscriptions.push(clientProvider) @@ -150,10 +154,13 @@ function activateLean4Features(context: ExtensionContext, installer: LeanInstall const projectOperationProvider: ProjectOperationProvider = new ProjectOperationProvider(installer.getOutputChannel(), clientProvider) + await setLeanFeatureSetActive(true) + return { clientProvider, infoProvider, projectOperationProvider } } export async function activate(context: ExtensionContext): Promise { + await setLeanFeatureSetActive(false) const alwaysEnabledFeatures: AlwaysEnabledFeatures = activateAlwaysEnabledFeatures(context) if (await isLean3Project(alwaysEnabledFeatures.installer)) { @@ -172,7 +179,7 @@ export async function activate(context: ExtensionContext): Promise { activateAbbreviationFeature(context, alwaysEnabledFeatures.docView) if (findOpenLeanDocument()) { - const lean4EnabledFeatures: Lean4EnabledFeatures = activateLean4Features(context, alwaysEnabledFeatures.installer) + const lean4EnabledFeatures: Lean4EnabledFeatures = await activateLean4Features(context, alwaysEnabledFeatures.installer) return { isLean4Project: true, version: '4', @@ -186,16 +193,16 @@ export async function activate(context: ExtensionContext): Promise { } // No Lean 4 document yet => Load remaining features when one is open - const disposeActivationListener: Disposable = workspace.onDidOpenTextDocument(doc => { + const disposeActivationListener: Disposable = workspace.onDidOpenTextDocument(async doc => { if (isLean(doc.languageId)) { - activateLean4Features(context, alwaysEnabledFeatures.installer) + await activateLean4Features(context, alwaysEnabledFeatures.installer) disposeActivationListener.dispose() } }, context.subscriptions) return { - isLean4Project: false, - version: undefined, + isLean4Project: true, + version: '4', infoProvider: undefined, clientProvider: undefined, projectOperationProvider: undefined, diff --git a/vscode-lean4/src/infoview.ts b/vscode-lean4/src/infoview.ts index 0fbe22fdf..4d7bf1ff9 100644 --- a/vscode-lean4/src/infoview.ts +++ b/vscode-lean4/src/infoview.ts @@ -281,7 +281,7 @@ export class InfoProvider implements Disposable { await this.sendPosition(); }), commands.registerTextEditorCommand('lean4.displayGoal', (editor) => this.openPreview(editor)), - commands.registerTextEditorCommand('lean4.toggleInfoview', (editor) => this.toggleInfoview(editor)), + commands.registerCommand('lean4.toggleInfoview', () => this.toggleInfoview()), commands.registerTextEditorCommand('lean4.displayList', async (editor) => { await this.openPreview(editor); await this.webviewPanel?.api.requestedAction({kind: 'toggleAllMessages'}); @@ -491,12 +491,14 @@ export class InfoProvider implements Disposable { this.rpcSessions = remaining } - private async toggleInfoview(editor: TextEditor) { + private async toggleInfoview() { if (this.webviewPanel) { this.webviewPanel.dispose(); // the onDispose handler sets this.webviewPanel = undefined + } else if (window.activeTextEditor && window.activeTextEditor.document.languageId === 'lean4') { + await this.openPreview(window.activeTextEditor); } else { - return this.openPreview(editor); + void window.showErrorMessage('No active Lean editor tab. Make sure to focus the Lean editor tab for which you want to open the infoview.') } } diff --git a/vscode-lean4/src/projectoperations.ts b/vscode-lean4/src/projectoperations.ts index 68c40cb73..f46a078aa 100644 --- a/vscode-lean4/src/projectoperations.ts +++ b/vscode-lean4/src/projectoperations.ts @@ -215,7 +215,7 @@ export class ProjectOperationProvider implements Disposable { private async determineDependencyToolchain(localToolchainPath: string, dependency: DirectGitDependency): Promise { const dependencyToolchainUri: Uri | undefined = this.determineDependencyToolchainUri(dependency.uri, dependency.inputRevision) if (!dependencyToolchainUri) { - const message = `Could not determine Lean version of ${dependency.name} at ${dependency.uri}, as doing so is currently only supported for GitHub projects. Do you want to update ${dependency.name} without updating the local Lean version to that of ${dependency.name} regardless?` + const message = `Could not determine Lean version of ${dependency.name} at ${dependency.uri}, as doing so is currently only supported for GitHub projects. Do you want to update ${dependency.name} without updating the Lean version of the open project to that of ${dependency.name} regardless?` const input = 'Proceed' const choice: string | undefined = await window.showInformationMessage(message, { modal: true}, input) return choice === 'input' ? 'DoNotUpdate' : 'Cancelled' @@ -224,9 +224,9 @@ export class ProjectOperationProvider implements Disposable { const toolchainResult = await this.fetchToolchains(localToolchainPath, dependencyToolchainUri) if (!(toolchainResult instanceof Array)) { const errorFlavor = toolchainResult === 'CannotReadLocalToolchain' - ? `Could not read local Lean version at '${localToolchainPath}'` + ? `Could not read Lean version of open project at '${localToolchainPath}'` : `Could not fetch Lean version of ${dependency.name} at ${dependency.uri}` - const message = `${errorFlavor}. Do you want to update ${dependency.name} without updating the local Lean version to that of ${dependency.name} regardless?` + const message = `${errorFlavor}. Do you want to update ${dependency.name} without updating the Lean version of the open project to that of ${dependency.name} regardless?` const input = 'Proceed' const choice: string | undefined = await window.showInformationMessage(message, { modal: true}, input) return choice === 'input' ? 'DoNotUpdate' : 'Cancelled' @@ -237,9 +237,9 @@ export class ProjectOperationProvider implements Disposable { return 'DoNotUpdate' } - const message = `'${localToolchain}' (local Lean version) differs from '${dependencyToolchain}' (${dependency.name} Lean version). Do you want to update the local Lean version to the Lean version of ${dependency.name}?` - const input1 = 'Update Local Version' - const input2 = 'Keep Local Version' + const message = `The Lean version '${localToolchain}' of the open project differs from the Lean version '${dependencyToolchain}' of ${dependency.name}. Do you want to update the Lean version of the open project to the Lean version of ${dependency.name}?` + const input1 = 'Update Lean Version' + const input2 = 'Keep Lean Version' const choice = await window.showInformationMessage(message, { modal: true }, input1, input2) if (choice === undefined) { return 'Cancelled' diff --git a/vscode-lean4/src/utils/clientProvider.ts b/vscode-lean4/src/utils/clientProvider.ts index 6f3e3ed23..e24f81c14 100644 --- a/vscode-lean4/src/utils/clientProvider.ts +++ b/vscode-lean4/src/utils/clientProvider.ts @@ -136,9 +136,17 @@ export class LeanClientProvider implements Disposable { } private restartFile() { - if (window.activeTextEditor && this.activeClient && window.activeTextEditor.document.languageId ==='lean4') { - void this.activeClient.restartFile(window.activeTextEditor.document); + if (!this.activeClient || !this.activeClient.isRunning()) { + void window.showErrorMessage('No active client.') + return } + + if (!window.activeTextEditor || window.activeTextEditor.document.languageId !== 'lean4') { + void window.showErrorMessage('No active Lean editor tab. Make sure to focus the Lean editor tab for which you want to issue a restart.') + return + } + + void this.activeClient.restartFile(window.activeTextEditor.document); } private stopActiveClient() { From bc7033a5322370867929479acc2518c66d69f3d3 Mon Sep 17 00:00:00 2001 From: mhuisi Date: Wed, 18 Oct 2023 10:43:07 +0200 Subject: [PATCH 17/18] update package-lock.json --- vscode-lean4/package-lock.json | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/vscode-lean4/package-lock.json b/vscode-lean4/package-lock.json index a0305afbe..66132b397 100644 --- a/vscode-lean4/package-lock.json +++ b/vscode-lean4/package-lock.json @@ -1,19 +1,20 @@ { "name": "lean4", - "version": "0.0.111", + "version": "0.0.113", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lean4", - "version": "0.0.111", + "version": "0.0.113", "license": "Apache-2.0", "dependencies": { "axios": "~0.24.0", "cheerio": "^1.0.0-rc.10", "mobx": "5.15.7", "semver": "=7.3.5", - "vscode-languageclient": "=8.0.2" + "vscode-languageclient": "=8.0.2", + "zod": "^3.22.4" }, "devDependencies": { "@types/cheerio": "~0.22.30", @@ -37,7 +38,7 @@ "webpack-cli": "^4.10.0" }, "engines": { - "vscode": "^1.70.0" + "vscode": "^1.75.0" } }, "node_modules/@babel/runtime": { @@ -4661,6 +4662,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } From 247d93a8a68f39e555a1d79df9662f5e390c2fe4 Mon Sep 17 00:00:00 2001 From: mhuisi Date: Wed, 18 Oct 2023 12:03:48 +0200 Subject: [PATCH 18/18] add support for prereleases --- .github/workflows/on-push.yml | 21 +++++++++++++++++++-- prerelease.sh | 13 +++++++++++++ vscode-lean4/package.json | 1 + 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100755 prerelease.sh diff --git a/.github/workflows/on-push.yml b/.github/workflows/on-push.yml index feab98fe4..975998d5f 100644 --- a/.github/workflows/on-push.yml +++ b/.github/workflows/on-push.yml @@ -43,7 +43,14 @@ jobs: npm ci npx lerna bootstrap --ci npm run build - npx lerna run --scope=lean4 package + + - name: Package + run: npx lerna run --scope=lean4 package + if: ${{ !startsWith(github.ref, 'refs/tags/v') || !endsWith(github.ref, '-pre') }} + + - name: Package pre-release + run: npx lerna run --scope=lean4 packagePreRelease + if: ${{ startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') }} - name: Upload artifact uses: actions/upload-artifact@v2 @@ -53,7 +60,7 @@ jobs: path: 'vscode-lean4/lean4-*.vsix' - name: Publish packaged extension - if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-latest' + if: ${{ startsWith(github.ref, 'refs/tags/v') && !endsWith(github.ref, '-pre') && matrix.os == 'ubuntu-latest' }} run: | cd vscode-lean4 npx vsce publish -i lean4-*.vsix @@ -62,6 +69,16 @@ jobs: OVSX_PAT: ${{ secrets.OVSX_PAT }} VSCE_PAT: ${{ secrets.VSCE_PAT }} + - name: Publish packaged pre-release extension + if: ${{ startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && matrix.os == 'ubuntu-latest' }} + run: | + cd vscode-lean4 + npx vsce publish --pre-release -i lean4-*.vsix + npx ovsx publish --pre-release lean4-*.vsix + env: + OVSX_PAT: ${{ secrets.OVSX_PAT }} + VSCE_PAT: ${{ secrets.VSCE_PAT }} + - name: Upload extension as release if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-latest' uses: softprops/action-gh-release@v1 diff --git a/prerelease.sh b/prerelease.sh new file mode 100755 index 000000000..38e994444 --- /dev/null +++ b/prerelease.sh @@ -0,0 +1,13 @@ +#!/bin/sh +if [ $# != 1 ]; then + echo Usage: ./prerelease.sh 1.2.3 + exit 1 +fi + +new_version="$1" +sed -i 's/"version": ".*"/"version": "'$new_version'"/' vscode-lean4/package.json +git commit -am "Release $new_version (pre-release)" +git tag -a v$new_version-pre -m "vscode-lean4 $new_version (pre-release)" + +git push +git push --tags diff --git a/vscode-lean4/package.json b/vscode-lean4/package.json index 13f8799a1..411f62449 100644 --- a/vscode-lean4/package.json +++ b/vscode-lean4/package.json @@ -874,6 +874,7 @@ "watch": "webpack --env development --watch", "watchTest": "concurrently \"tsc -p . -w --outDir out\" \"npm run watch\"", "package": "vsce package", + "packagePreRelease": "vsce package --pre-release", "pretest": "tsc -p . --outDir out", "test": "node ./out/test/suite/runTest.js" },