lean4-code-actions
Version:
Refactorings and snippets for Lean 4
237 lines (217 loc) • 10.6 kB
text/typescript
import { ensureNonEmptyArray, isNonEmptyArray } from 'libs/utils/array/ensureNonEmptyArray'
import path from 'path'
import { concat, identity } from 'remeda'
import { FileBrand, FileBrandSchema } from 'src/models/FileBrand'
import { FileContentVariety } from 'src/models/FileContentVariety'
import { FileInfo, getFileInfo, getNewUri } from 'src/models/FileInfo'
import { leanNameSeparator, toString } from 'src/models/Lean/HieroName'
import { Name, splitNames } from 'src/models/Lean/Name'
import { NewTypeKeyword, NewTypeKeywordSchema } from 'src/models/NewTypeKeyword'
import { replaceSnippetVariables } from 'src/utils/SnippetString'
import { getRelativePathFromUri } from 'src/utils/Uri'
import { CreateNewFileConfig } from 'src/utils/WorkspaceConfiguration/CreateFileConfig'
import { createFileIfNotExists } from 'src/utils/WorkspaceEdit'
import { combineFileContent, Line, trimEmpty } from 'src/utils/text'
import { getTopLevelDirectoryEntries } from 'src/utils/workspace'
import { commands, QuickPickItem, QuickPickItemKind, TextEditor, Uri, window, workspace } from 'vscode'
import { isExcluded, isHidden, leanFileExtensionLong } from '../constants'
import { getImportLinesFromStrings, getOpenLinesFromStrings } from '../models/Lean/SyntaxNodes'
import { getDeclarationSnippetLines } from '../utils/Lean/SnippetString'
import { StaticQuickPickItem } from '../utils/QuickPickItem'
import { ensureEditor, getSelectedName, getSelectedNames } from '../utils/TextEditor'
import { withImportsOpens, withImportsOpensDerivings } from '../utils/WorkspaceConfiguration/withImportsOpensDerivings'
import { getUntilIsValidP } from '../../libs/utils/Getter/getUntilValid'
export async function createNewFile() {
const config = workspace.getConfiguration('lean4CodeActions.createNewFile')
const editor = ensureEditor()
const lib = await askLibFromEditor(editor)
if (lib === undefined) return
const names = await askNamesFromEditor(editor)
if (names === undefined) return
const [namespace, name] = splitNames(names)
const variety = await askFileContentVarietyFromEditor(editor)
if (variety === undefined) return
const { brand, keyword } = variety
const tags = brand ? [brand] : []
const info: FileInfo = { lib, namespace, name, tags }
const uri = getNewUri(editor.document.uri, info)
const contents = getTypeFileContentsFromConfigV2(config)(info, keyword)
await createFileIfNotExists(uri, contents)
await commands.executeCommand('vscode.open', uri)
}
const withNamespacePrefix = (names: Name[]) => {
const namespaceConfig = workspace.getConfiguration('lean4CodeActions.namespace')
const prefix = namespaceConfig.get<string>('prefix')
if (!prefix) return names
return concat(prefix.split(leanNameSeparator), names)
}
export const askFilenameFromEditor = async (editor: TextEditor) => {
const newName = getSelectedName(editor) ?? 'New'
// TODO: validate path
return askFilename(newName, editor.document.uri)
}
export const askNamesFromEditor = async (editor: TextEditor) => {
// const newName = getSelectedName(editor) ?? 'New'
return askNames(editor.document.uri)
}
export const askLibFromEditor = async (editor: TextEditor) => {
const defaultLib = workspace.getConfiguration('lean4CodeActions').get<string>('defaultLib')
if (defaultLib) return defaultLib
return askLib(editor.document.uri)
}
export const askFileContentVarietyFromEditor = async (editor: TextEditor) => {
const names = getSelectedNames(editor) ?? []
return askFileContentVariety(names)(editor.document.uri)
}
export const askFileContentVariety = (names: Name[]) => async (currentDocumentUri: Uri) => {
const typeBrand: FileBrand = 'type'
const typeVarietyQuickPickItems = NewTypeKeywordSchema.options.map<StaticQuickPickItem<FileContentVariety>>(keyword => ({
label: `${typeBrand} (${keyword})`,
value: { brand: typeBrand, keyword },
}))
const brandVarietyQuickPickItems = FileBrandSchema.options.filter(brand => brand !== typeBrand).map<StaticQuickPickItem<FileContentVariety>>(brand => ({
label: brand,
value: { brand, keyword: null },
}))
const varietyQuickPickItems = concat(typeVarietyQuickPickItems, brandVarietyQuickPickItems)
const result = await window.showQuickPick(varietyQuickPickItems, {
title: 'Pick a file content variety',
})
return result && result.value
}
export async function askNames(currentDocumentUri: Uri) {
const info = getFileInfo(workspace.asRelativePath(currentDocumentUri))
const { namespace, name } = info ?? { namespace: [], name: 'New' }
const parentNamespace = toString(namespace)
const value = parentNamespace ? parentNamespace + leanNameSeparator + name : name
const valueSelection: [number, number] = [value.length - name.length, value.length]
const result = await window.showInputBox({
title: 'Fully qualified Lean name',
value,
valueSelection,
})
if (!result) return undefined
const names = result.split(leanNameSeparator).filter(identity)
return ensureNonEmptyArray(names)
}
// async function getImportsOpensDerivingsViaSubcommands(keyword: NewTypeKeyword, names: Name[]) {
// const subcommand = executeSubcommandIfExists('lean4CodeActions.createNewType', keyword, names)
// const imports = (await subcommand<Imports>('getImports')) || []
// const opens = (await subcommand<Opens>('getOpens')) || []
// const derivings = (await subcommand<Derivings>('getDerivings')) || []
// }
async function askKeyword() {
const keywordQuickPickItems = NewTypeKeywordSchema.options.map<StaticQuickPickItem<NewTypeKeyword | null>>(keyword => ({
label: keyword,
value: keyword,
picked: keyword === 'structure',
})).concat({
label: '(none)',
value: null,
})
const keywordResult = await window.showQuickPick(keywordQuickPickItems, {
title: 'Pick a keyword for the definition',
})
return keywordResult && keywordResult.value
}
export async function askLib(currentDocumentUri: Uri) {
const relativePath = getRelativePathFromUri(currentDocumentUri)
const defaultLib = relativePath.split(path.sep)[1]
const libEntries = await getTopLevelDirectoryEntries(currentDocumentUri)
if (!libEntries) throw new Error(`Cannot get top level directory paths from uri: ${currentDocumentUri}`)
const libs = libEntries.filter(lib => !isHidden(lib) && !isExcluded(lib) && lib !== defaultLib)
// const currentDocumentNames = getLeanNamesFromUri(currentDocumentUri)
// const currentDocumentParentNames = currentDocumentNames.slice(0, -1)
// const parentNamespace = toString(currentDocumentParentNames)
// const value = parentNamespace ? parentNamespace + leanNameSeparator + newName : newName
// const valueSelection: [number, number] = [value.length - newName.length, value.length]
const defaultLibItems: QuickPickItem[] = defaultLib ? [
{
label: defaultLib,
kind: QuickPickItemKind.Default,
},
{
label: '',
kind: QuickPickItemKind.Separator,
},
] : []
const libItems: QuickPickItem[] = libs.map<QuickPickItem>(lib => ({
label: lib,
kind: QuickPickItemKind.Default,
}))
const items = concat(defaultLibItems, libItems)
const result = await window.showQuickPick(items, { title: 'Pick a library' })
return result?.label
}
export const askFilename = async (name: string, currentDocumentUri: Uri) => {
const pathname = getRelativePathFromUri(currentDocumentUri)
const { dir } = path.parse(pathname)
// TODO: validate path in a loop
const prefix = dir.substring(1) + path.sep
const suffix = leanFileExtensionLong
const value = prefix + name + suffix
const valueSelection: [number, number] = [prefix.length, prefix.length + name.length]
const isValid = async (filepath: string | undefined) => {
if (filepath === undefined) return false
const { ext } = path.parse(filepath)
return ext === leanFileExtensionLong
}
const get = async () => window.showInputBox({
title: 'New file path',
value,
valueSelection,
})
return getUntilIsValidP(get, isValid)()
}
export const getTypeFileContentsCV1 = (config: CreateNewFileConfig) => getTypeFileContentsV1(config.imports, config.opens, config.derivings)
export const wrapFileContentsV1 = (imports: string[], opens: string[]) => (parents: Name[], name: Name) => (contentsLines: Line[]) => {
const importsLines = getImportLinesFromStrings(imports)
const parentNamespaceLines = [`namespace ${toString(parents)}`]
const opensLines = getOpenLinesFromStrings(opens)
const childNamespaceLines = [`namespace ${name}`]
return combineFileContent([
importsLines,
parentNamespaceLines,
opensLines,
contentsLines,
childNamespaceLines,
].filter(isNonEmptyArray))
}
export const wrapFileContentsV2 = (imports: string[], opens: string[]) => (info: FileInfo, keyword: NewTypeKeyword | null) => (contentsLines: Line[]) => {
const { lib, namespace, name, tags } = info
const importsLines = getImportLinesFromStrings(imports)
const opensLines = getOpenLinesFromStrings(opens)
if (keyword) {
const parentNamespaceLines = [`namespace ${toString(namespace)}`]
const childNamespaceLines = [`namespace ${name}`]
return combineFileContent([
importsLines,
parentNamespaceLines,
opensLines,
contentsLines,
childNamespaceLines,
].filter(isNonEmptyArray))
} else {
const names = [...namespace, name]
const namespaceLines = [`namespace ${toString(names)}`]
return combineFileContent([
importsLines,
namespaceLines,
opensLines,
contentsLines,
].filter(isNonEmptyArray))
}
}
export const getTypeFileContentsV1 = (imports: string[], opens: string[], derivings: string[]) => (keyword: NewTypeKeyword | null, parents: Name[], name: Name) => {
const declarationSnippetLines = getDeclarationSnippetLines(derivings, keyword)
const declarationLines = trimEmpty(replaceSnippetVariables(['$1', name, '$1'])(declarationSnippetLines))
return wrapFileContentsV1(imports, opens)(parents, name)(declarationLines)
}
export const getTypeFileContentsV2 = (imports: string[], opens: string[], derivings: string[]) => (info: FileInfo, keyword: NewTypeKeyword | null) => {
const { name } = info
const declarationSnippetLines = getDeclarationSnippetLines(derivings, keyword)
const declarationLines = trimEmpty(replaceSnippetVariables(['$1', name, '$1'])(declarationSnippetLines))
return wrapFileContentsV2(imports, opens)(info, keyword)(declarationLines)
}
export const getTypeFileContentsFromConfigV2 = withImportsOpensDerivings(getTypeFileContentsV2)
export const wrapFileContentsFromConfigV2 = withImportsOpens(wrapFileContentsV2)