Skip to content

Commit

Permalink
Update lower-right corner units menu to read and edit inline settings…
Browse files Browse the repository at this point in the history
… annotations if present (#5212)

* WIP show annotation length unit setting in LowerRightControls if present

* Add logic for changing settings annotation if it's present

* Add E2E test

* Cleanup lints, fmt, tsc, logs

* Change to use settings from Rust helper function

- Fix thrown error to use the cause field
- Fix function names to not use "get"

* Remove unneeded constants

* Post-merge fixups

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Add back `ImportStatement` to make tsc happy (thanks @jtran!)

* fmt

---------

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
4 people authored Feb 6, 2025
1 parent 019cb81 commit 1c0a38a
Show file tree
Hide file tree
Showing 9 changed files with 262 additions and 15 deletions.
49 changes: 49 additions & 0 deletions e2e/playwright/testing-settings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -896,4 +896,53 @@ test.describe('Testing settings', () => {
})
}
)

test(`Change inline units setting`, async ({
page,
homePage,
context,
editor,
}) => {
const initialInlineUnits = 'yd'
const editedInlineUnits = { short: 'mm', long: 'Millimeters' }
const inlineSettingsString = (s: string) =>
`@settings(defaultLengthUnit = ${s})`
const unitsIndicator = page.getByRole('button', {
name: 'Current units are:',
})
const unitsChangeButton = (name: string) =>
page.getByRole('button', { name, exact: true })

await context.folderSetupFn(async (dir) => {
const bracketDir = join(dir, 'project-000')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('cube.kcl'),
join(bracketDir, 'main.kcl')
)
})

await test.step(`Initial units from settings`, async () => {
await homePage.openProject('project-000')
await expect(unitsIndicator).toHaveText('Current units are: in')
})

await test.step(`Manually write inline settings`, async () => {
await editor.openPane()
await editor.replaceCode(
`fn cube`,
`${inlineSettingsString(initialInlineUnits)}
fn cube`
)
await expect(unitsIndicator).toContainText(initialInlineUnits)
})

await test.step(`Change units setting via lower-right control`, async () => {
await unitsIndicator.click()
await unitsChangeButton(editedInlineUnits.long).click()
await expect(
page.getByText(`Updated per-file units to ${editedInlineUnits.short}`)
).toBeVisible()
})
})
})
62 changes: 53 additions & 9 deletions src/components/UnitsMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,31 @@
import { Popover } from '@headlessui/react'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { changeKclSettings, unitLengthToUnitLen } from 'lang/wasm'
import { baseUnitLabels, baseUnitsUnion } from 'lib/settings/settingsTypes'
import { codeManager, kclManager } from 'lib/singletons'
import { err, reportRejection } from 'lib/trap'
import { useEffect, useState } from 'react'
import toast from 'react-hot-toast'

export function UnitsMenu() {
const { settings } = useSettingsAuthContext()
const [hasPerFileLengthUnit, setHasPerFileLengthUnit] = useState(
Boolean(kclManager.fileSettings.defaultLengthUnit)
)
const [lengthSetting, setLengthSetting] = useState(
kclManager.fileSettings.defaultLengthUnit ||
settings.context.modeling.defaultUnit.current
)
useEffect(() => {
setHasPerFileLengthUnit(Boolean(kclManager.fileSettings.defaultLengthUnit))
setLengthSetting(
kclManager.fileSettings.defaultLengthUnit ||
settings.context.modeling.defaultUnit.current
)
}, [
kclManager.fileSettings.defaultLengthUnit,
settings.context.modeling.defaultUnit.current,
])
return (
<Popover className="relative pointer-events-auto">
{({ close }) => (
Expand All @@ -18,7 +40,7 @@ export function UnitsMenu() {
<div className="absolute w-[1px] h-[1em] bg-primary right-0 top-1/2 -translate-y-1/2"></div>
</div>
<span className="sr-only">Current units are:&nbsp;</span>
{settings.context.modeling.defaultUnit.current}
{lengthSetting}
</Popover.Button>
<Popover.Panel
className={`absolute bottom-full right-0 mb-2 w-48 bg-chalkboard-10 dark:bg-chalkboard-90
Expand All @@ -31,18 +53,40 @@ export function UnitsMenu() {
<button
className="flex items-center gap-2 m-0 py-1.5 px-2 cursor-pointer hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80 border-none text-left"
onClick={() => {
settings.send({
type: 'set.modeling.defaultUnit',
data: {
level: 'project',
value: unit,
},
})
if (hasPerFileLengthUnit) {
const newCode = changeKclSettings(codeManager.code, {
defaultLengthUnits: unitLengthToUnitLen(unit),
defaultAngleUnits: { type: 'Degrees' },
})
if (err(newCode)) {
toast.error(
`Failed to set per-file units: ${newCode.message}`
)
} else {
codeManager.updateCodeStateEditor(newCode)
Promise.all([
codeManager.writeToFile(),
kclManager.executeCode(),
])
.then(() => {
toast.success(`Updated per-file units to ${unit}`)
})
.catch(reportRejection)
}
} else {
settings.send({
type: 'set.modeling.defaultUnit',
data: {
level: 'project',
value: unit,
},
})
}
close()
}}
>
<span className="flex-1">{baseUnitLabels[unit]}</span>
{unit === settings.context.modeling.defaultUnit.current && (
{unit === lengthSetting && (
<span className="text-chalkboard-60">current</span>
)}
</button>
Expand Down
19 changes: 18 additions & 1 deletion src/lang/KclSingleton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
SourceRange,
topLevelRange,
} from 'lang/wasm'
import { getNodeFromPath } from './queryAst'
import { getNodeFromPath, getSettingsAnnotation } from './queryAst'
import { codeManager, editorManager, sceneInfra } from 'lib/singletons'
import { Diagnostic } from '@codemirror/lint'
import { markOnce } from 'lib/performance'
Expand All @@ -35,6 +35,7 @@ import {
ModelingCmdReq_type,
} from '@kittycad/lib/dist/types/src/models'
import { Operation } from 'wasm-lib/kcl/bindings/Operation'
import { KclSettingsAnnotation } from 'lib/settings/settingsTypes'

interface ExecuteArgs {
ast?: Node<Program>
Expand Down Expand Up @@ -70,6 +71,7 @@ export class KclManager {
private _wasmInitFailed = true
private _hasErrors = false
private _switchedFiles = false
private _fileSettings: KclSettingsAnnotation = {}

engineCommandManager: EngineCommandManager

Expand Down Expand Up @@ -368,6 +370,13 @@ export class KclManager {
await this.disableSketchMode()
}

let fileSettings = getSettingsAnnotation(ast)
if (err(fileSettings)) {
console.error(fileSettings)
fileSettings = {}
}
this.fileSettings = fileSettings

this.logs = logs
this.errors = errors
// Do not add the errors since the program was interrupted and the error is not a real KCL error
Expand Down Expand Up @@ -699,6 +708,14 @@ export class KclManager {
_isAstEmpty(ast: Node<Program>) {
return ast.start === 0 && ast.end === 0 && ast.body.length === 0
}

get fileSettings() {
return this._fileSettings
}

set fileSettings(settings: KclSettingsAnnotation) {
this._fileSettings = settings
}
}

const defaultSelectionFilter: EntityType_type[] = [
Expand Down
25 changes: 25 additions & 0 deletions src/lang/queryAst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import {
VariableDeclaration,
VariableDeclarator,
recast,
kclSettings,
unitLenToUnitLength,
unitAngToUnitAngle,
} from './wasm'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { createIdentifier, splitPathAtLastIndex } from './modifyAst'
Expand All @@ -38,6 +41,7 @@ import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { findKwArg } from './util'
import { codeRefFromRange } from './std/artifactGraph'
import { KclSettingsAnnotation } from 'lib/settings/settingsTypes'

export const LABELED_ARG_FIELD = 'LabeledArg -> Arg'
export const ARG_INDEX_FIELD = 'arg index'
Expand Down Expand Up @@ -866,3 +870,24 @@ export function getObjExprProperty(
if (index === -1) return null
return { expr: node.properties[index].value, index }
}

/**
* Given KCL, returns the settings annotation object if it exists.
*/
export function getSettingsAnnotation(
kcl: string | Node<Program>
): KclSettingsAnnotation | Error {
const metaSettings = kclSettings(kcl)
if (err(metaSettings)) return metaSettings

const settings: KclSettingsAnnotation = {}
// No settings in the KCL.
if (!metaSettings) return settings

settings.defaultLengthUnit = unitLenToUnitLength(
metaSettings.defaultLengthUnits
)
settings.defaultAngleUnit = unitAngToUnitAngle(metaSettings.defaultAngleUnits)

return settings
}
91 changes: 87 additions & 4 deletions src/lang/wasm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
default_project_settings,
base64_decode,
clear_scene_and_bust_cache,
kcl_settings,
change_kcl_settings,
reloadModule,
} from 'lib/wasm_lib_wrapper'
Expand Down Expand Up @@ -58,6 +59,9 @@ import { Artifact } from './std/artifactGraph'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { NumericSuffix } from 'wasm-lib/kcl/bindings/NumericSuffix'
import { MetaSettings } from 'wasm-lib/kcl/bindings/MetaSettings'
import { UnitAngle, UnitLength } from 'wasm-lib/kcl/bindings/ModelingCmd'
import { UnitLen } from 'wasm-lib/kcl/bindings/UnitLen'
import { UnitAngle as UnitAng } from 'wasm-lib/kcl/bindings/UnitAngle'

export type { Artifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact'
Expand Down Expand Up @@ -857,16 +861,95 @@ export function base64Decode(base64: string): ArrayBuffer | Error {
}
}

/// Change the meta settings for the kcl file.
/// Returns the new kcl string with the updated settings.
/**
* Get the meta settings for the KCL. If no settings were set in the file,
* returns null.
*/
export function kclSettings(
kcl: string | Node<Program>
): MetaSettings | null | Error {
let program: Node<Program>
if (typeof kcl === 'string') {
const parseResult = parse(kcl)
if (err(parseResult)) return parseResult
if (!resultIsOk(parseResult)) {
return new Error(`parse result had errors`, { cause: parseResult })
}
program = parseResult.program
} else {
program = kcl
}
try {
return kcl_settings(JSON.stringify(program))
} catch (e) {
return new Error('Caught error getting kcl settings', { cause: e })
}
}

/**
* Change the meta settings for the kcl file.
* @returns the new kcl string with the updated settings.
*/
export function changeKclSettings(
kcl: string,
settings: MetaSettings
): string | Error {
try {
return change_kcl_settings(kcl, JSON.stringify(settings))
} catch (e) {
console.error('Caught error changing kcl settings: ' + e)
return new Error('Caught error changing kcl settings: ' + e)
console.error('Caught error changing kcl settings', e)
return new Error('Caught error changing kcl settings', { cause: e })
}
}

/**
* Convert a `UnitLength_type` to a `UnitLen`
*/
export function unitLengthToUnitLen(input: UnitLength): UnitLen {
switch (input) {
case 'm':
return { type: 'M' }
case 'cm':
return { type: 'Cm' }
case 'yd':
return { type: 'Yards' }
case 'ft':
return { type: 'Feet' }
case 'in':
return { type: 'Inches' }
default:
return { type: 'Mm' }
}
}

/**
* Convert `UnitLen` to `UnitLength_type`.
*/
export function unitLenToUnitLength(input: UnitLen): UnitLength {
switch (input.type) {
case 'M':
return 'm'
case 'Cm':
return 'cm'
case 'Yards':
return 'yd'
case 'Feet':
return 'ft'
case 'Inches':
return 'in'
default:
return 'mm'
}
}

/**
* Convert `UnitAngle` to `UnitAngle_type`.
*/
export function unitAngToUnitAngle(input: UnitAng): UnitAngle {
switch (input.type) {
case 'Radians':
return 'radians'
default:
return 'degrees'
}
}
13 changes: 13 additions & 0 deletions src/lib/settings/settingsTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { AtLeast, PathValue, Paths } from 'lib/types'
import { CommandArgumentConfig } from 'lib/commandTypes'
import { Themes } from 'lib/theme'
import { CameraProjectionType } from 'wasm-lib/kcl/bindings/CameraProjectionType'
import {
UnitAngle_type,
UnitLength_type,
} from '@kittycad/lib/dist/types/src/models'
import { CameraOrbitType } from 'wasm-lib/kcl/bindings/CameraOrbitType'

export interface SettingsViaQueryString {
Expand Down Expand Up @@ -138,3 +142,12 @@ type RecursiveSettingsPayloads<T> = {
}

export type SaveSettingsPayload = RecursiveSettingsPayloads<typeof settings>

/**
* Annotation names for default units are defined on rust side in
* src/wasm-lib/kcl/src/execution/annotations.rs
*/
export interface KclSettingsAnnotation {
defaultLengthUnit?: UnitLength_type
defaultAngleUnit?: UnitAngle_type
}
Loading

0 comments on commit 1c0a38a

Please sign in to comment.