* WIP: Add point-and-click Import for geometry Will eventually fix #6120 Right now the whole loop is there but the codemod doesn't work yet * Better pathToNOde, log on non-working cm dispatch call * Add workaround to updateModelingState not working * Back to updateModelingState with a skip flag * Better todo * Change working from Import to Insert, cleanups * Sister command in kclCommands to populate file options * Improve path selector * Unsure: move importAstMod to kclCommands onSubmit 😶 * Add e2e test * Clean up for review * Add native file menu entry and test * No await yo lint said so * WIP: UX improvements around foreign file imports Fixes #6152 * @lrev-Dev's suggestion to remove a comment Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch> * Update to scene.settled(cmdBar) * Add partNNN default name for alias * Lint * Lint * Fix unit tests * Add sad path insert test Thanks @Irev-Dev for the suggestion * Add step insert test * Lint * Add test for second foreign import thru file tree click * WIP: Add point-and-click Load to copy files from outside the project into the project Towards #6210 * Move Insert button to modeling toolbar, update menus and toolbars * Add default value for local name alias * Aligning tests * Fix tests * Add padding for filenames starting with a digit * Lint * Lint * Update snapshots * Merge branch 'main' into pierremtb/issue6210-Add-point-and-click-Load-to-copy-files-from-outside-the-project-into-the-project * Add disabled transform subbutton * Merge kcl-samples and local disk load into one 'Load external model' command * Fix em tests * Fix test * Add test for file pick import, better input * Fix non .kcl loading * Lint * Update snapshots * Fix issue leading to test failure * Fix clone test * Add note * Fix nested clone issue * Clean up for review * Add valueSummary for path * Fix test after path change * Clean up for review * Update src/lib/kclCommands.ts Thanks @franknoirot! Co-authored-by: Frank Noirot <frank@zoo.dev> * Improve path input arg * Fix tests * Merge branch 'main' into pierremtb/issue6210-Add-point-and-click-Load-to-copy-files-from-outside-the-project-into-the-project * Fix path header not showing and improve tests * Clean up --------- Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch> Co-authored-by: Frank Noirot <frank@zoo.dev>
250 lines
6.9 KiB
TypeScript
250 lines
6.9 KiB
TypeScript
import { relevantFileExtensions } from '@src/lang/wasmUtils'
|
|
import {
|
|
FILE_EXT,
|
|
INDEX_IDENTIFIER,
|
|
MAX_PADDING,
|
|
ONBOARDING_PROJECT_NAME,
|
|
} from '@src/lib/constants'
|
|
import {
|
|
createNewProjectDirectory,
|
|
listProjects,
|
|
readAppSettingsFile,
|
|
} from '@src/lib/desktop'
|
|
import { bracket } from '@src/lib/exampleKcl'
|
|
import { isDesktop } from '@src/lib/isDesktop'
|
|
import { PATHS } from '@src/lib/paths'
|
|
import type { FileEntry } from '@src/lib/project'
|
|
|
|
export const isHidden = (fileOrDir: FileEntry) =>
|
|
!!fileOrDir.name?.startsWith('.')
|
|
|
|
export const isDir = (fileOrDir: FileEntry) =>
|
|
'children' in fileOrDir && fileOrDir.children !== undefined
|
|
|
|
// Shallow sort the files and directories
|
|
// Files and directories are sorted alphabetically
|
|
export function sortFilesAndDirectories(files: FileEntry[]): FileEntry[] {
|
|
return files.sort((a, b) => {
|
|
if (a.children === null && b.children !== null) {
|
|
return 1
|
|
} else if (a.children !== null && b.children === null) {
|
|
return -1
|
|
} else if (a.name && b.name) {
|
|
return a.name.localeCompare(b.name)
|
|
} else {
|
|
return 0
|
|
}
|
|
})
|
|
}
|
|
|
|
// create a regex to match the project name
|
|
// replacing any instances of "$n" with a regex to match any number
|
|
function interpolateProjectName(projectName: string) {
|
|
const regex = new RegExp(
|
|
projectName.replace(getPaddedIdentifierRegExp(), '([0-9]+)')
|
|
)
|
|
return regex
|
|
}
|
|
|
|
// Returns the next available index for a project name
|
|
export function getNextProjectIndex(
|
|
projectName: string,
|
|
projects: FileEntry[]
|
|
) {
|
|
const regex = interpolateProjectName(projectName)
|
|
const matches = projects.map((project) => project.name?.match(regex))
|
|
const indices = matches
|
|
.filter(Boolean)
|
|
.map((match) => (match !== null ? match[1] : '-1'))
|
|
.map((maybeMatchIndex) => {
|
|
return parseInt(maybeMatchIndex || '0', 10)
|
|
})
|
|
const maxIndex = Math.max(...indices, -1)
|
|
return maxIndex + 1
|
|
}
|
|
|
|
// Interpolates the project name with the next available index,
|
|
// padding the index with 0s if necessary
|
|
export function interpolateProjectNameWithIndex(
|
|
projectName: string,
|
|
index: number
|
|
) {
|
|
const regex = getPaddedIdentifierRegExp()
|
|
|
|
const matches = projectName.match(regex)
|
|
const padStartLength = Math.min(
|
|
matches !== null ? matches[1]?.length || 0 : 0,
|
|
MAX_PADDING
|
|
)
|
|
return projectName.replace(
|
|
regex,
|
|
index.toString().padStart(padStartLength + 1, '0')
|
|
)
|
|
}
|
|
|
|
export function doesProjectNameNeedInterpolated(projectName: string) {
|
|
return projectName.includes(INDEX_IDENTIFIER)
|
|
}
|
|
|
|
/**
|
|
* Given a target name, which may include our magic index interpolation string,
|
|
* and a list of projects, return a unique name that doesn't conflict with any
|
|
* of the existing projects, incrementing any ending number if necessary.
|
|
* @param name
|
|
* @param projects
|
|
* @returns
|
|
*/
|
|
export function getUniqueProjectName(name: string, projects: FileEntry[]) {
|
|
// The name may have our magic index interpolation string in it
|
|
const needsInterpolation = doesProjectNameNeedInterpolated(name)
|
|
|
|
if (needsInterpolation) {
|
|
const nextIndex = getNextProjectIndex(name, projects)
|
|
return interpolateProjectNameWithIndex(name, nextIndex)
|
|
} else {
|
|
let newName = name
|
|
while (projects.some((project) => project.name === newName)) {
|
|
const nameEndsWithNumber = newName.match(/\d+$/)
|
|
newName = nameEndsWithNumber
|
|
? newName.replace(/\d+$/, (num) => `${parseInt(num, 10) + 1}`)
|
|
: `${name}-1`
|
|
}
|
|
return newName
|
|
}
|
|
}
|
|
|
|
function escapeRegExpChars(string: string) {
|
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
}
|
|
|
|
function getPaddedIdentifierRegExp() {
|
|
const escapedIdentifier = escapeRegExpChars(INDEX_IDENTIFIER)
|
|
return new RegExp(`${escapedIdentifier}(${escapedIdentifier.slice(-1)}*)`)
|
|
}
|
|
|
|
export async function getSettingsFolderPaths(projectPath?: string) {
|
|
const user = isDesktop() ? await window.electron.getPath('appData') : '/'
|
|
const project = projectPath !== undefined ? projectPath : undefined
|
|
|
|
return {
|
|
user,
|
|
project,
|
|
}
|
|
}
|
|
|
|
export async function createAndOpenNewTutorialProject({
|
|
onProjectOpen,
|
|
navigate,
|
|
}: {
|
|
onProjectOpen: (
|
|
project: {
|
|
name: string | null
|
|
path: string | null
|
|
} | null,
|
|
file: FileEntry | null
|
|
) => void
|
|
navigate: (path: string) => void
|
|
}) {
|
|
// Create a new project with the onboarding project name
|
|
const configuration = await readAppSettingsFile()
|
|
const projects = await listProjects(configuration)
|
|
const nextIndex = getNextProjectIndex(ONBOARDING_PROJECT_NAME, projects)
|
|
const name = interpolateProjectNameWithIndex(
|
|
ONBOARDING_PROJECT_NAME,
|
|
nextIndex
|
|
)
|
|
|
|
// Delete the tutorial project if it already exists.
|
|
if (isDesktop()) {
|
|
if (configuration.settings?.project?.directory === undefined) {
|
|
return Promise.reject(new Error('configuration settings are undefined'))
|
|
}
|
|
|
|
const fullPath = window.electron.join(
|
|
configuration.settings.project.directory,
|
|
name
|
|
)
|
|
if (window.electron.exists(fullPath)) {
|
|
await window.electron.rm(fullPath)
|
|
}
|
|
}
|
|
|
|
const newProject = await createNewProjectDirectory(
|
|
name,
|
|
bracket,
|
|
configuration
|
|
)
|
|
|
|
// Prep the LSP and navigate to the onboarding start
|
|
onProjectOpen(
|
|
{
|
|
name: newProject.name,
|
|
path: newProject.path,
|
|
},
|
|
null
|
|
)
|
|
navigate(
|
|
`${PATHS.FILE}/${encodeURIComponent(newProject.default_file)}${
|
|
PATHS.ONBOARDING.INDEX
|
|
}`
|
|
)
|
|
return newProject
|
|
}
|
|
|
|
/**
|
|
* Get the next available file name by appending a hyphen and number to the end of the name
|
|
*/
|
|
export function getNextFileName({
|
|
entryName,
|
|
baseDir,
|
|
}: {
|
|
entryName: string
|
|
baseDir: string
|
|
}) {
|
|
// Preserve the extension in case of a relevant but foreign file
|
|
let extension = window.electron.path.extname(entryName)
|
|
if (!relevantFileExtensions().includes(extension.replace('.', ''))) {
|
|
extension = FILE_EXT
|
|
}
|
|
|
|
// Remove any existing index from the name before adding a new one
|
|
let createdName = entryName.replace(extension, '') + extension
|
|
let createdPath = window.electron.path.join(baseDir, createdName)
|
|
let i = 1
|
|
while (window.electron.exists(createdPath)) {
|
|
const matchOnIndexAndExtension = new RegExp(`(-\\d+)?(${extension})?$`)
|
|
createdName =
|
|
entryName.replace(matchOnIndexAndExtension, '') + `-${i}` + extension
|
|
createdPath = window.electron.path.join(baseDir, createdName)
|
|
i++
|
|
}
|
|
return {
|
|
name: createdName,
|
|
path: createdPath,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the next available directory name by appending a hyphen and number to the end of the name
|
|
*/
|
|
export function getNextDirName({
|
|
entryName,
|
|
baseDir,
|
|
}: {
|
|
entryName: string
|
|
baseDir: string
|
|
}) {
|
|
let createdName = entryName
|
|
let createdPath = window.electron.path.join(baseDir, createdName)
|
|
let i = 1
|
|
while (window.electron.exists(createdPath)) {
|
|
createdName = entryName.replace(/-\d+$/, '') + `-${i}`
|
|
createdPath = window.electron.path.join(baseDir, createdName)
|
|
i++
|
|
}
|
|
return {
|
|
name: createdName,
|
|
path: createdPath,
|
|
}
|
|
}
|