2024-07-02 17:16:27 +10:00
|
|
|
import { executeAst, lintAst } from 'lang/langHelpers'
|
2023-10-16 21:20:05 +11:00
|
|
|
import { Selections } from 'lib/selections'
|
2024-04-19 14:24:40 -07:00
|
|
|
import { KCLError, kclErrorsToDiagnostics } from './errors'
|
2024-04-03 19:38:16 +11:00
|
|
|
import { uuidv4 } from 'lib/utils'
|
2024-03-22 16:55:30 +11:00
|
|
|
import { EngineCommandManager } from './std/engineConnection'
|
2024-06-24 11:45:40 -04:00
|
|
|
import { err } from 'lib/trap'
|
2024-08-30 05:14:24 -05:00
|
|
|
import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
|
2024-03-22 16:55:30 +11:00
|
|
|
|
2023-10-11 13:36:54 +11:00
|
|
|
import {
|
2024-02-11 12:59:00 +11:00
|
|
|
CallExpression,
|
2023-10-11 13:36:54 +11:00
|
|
|
initPromise,
|
|
|
|
parse,
|
|
|
|
PathToNode,
|
|
|
|
Program,
|
|
|
|
ProgramMemory,
|
|
|
|
recast,
|
2024-07-04 01:19:24 -04:00
|
|
|
SourceRange,
|
2023-10-11 13:36:54 +11:00
|
|
|
} from 'lang/wasm'
|
|
|
|
import { getNodeFromPath } from './queryAst'
|
2024-05-06 19:28:30 +10:00
|
|
|
import { codeManager, editorManager, sceneInfra } from 'lib/singletons'
|
2024-07-29 08:41:02 -07:00
|
|
|
import { Diagnostic } from '@codemirror/lint'
|
2023-10-11 13:36:54 +11:00
|
|
|
|
2024-08-14 08:49:00 -07:00
|
|
|
interface ExecuteArgs {
|
|
|
|
ast?: Program
|
|
|
|
zoomToFit?: boolean
|
|
|
|
executionId?: number
|
|
|
|
zoomOnRangeAndType?: {
|
|
|
|
range: SourceRange
|
|
|
|
type: string
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-22 16:55:30 +11:00
|
|
|
export class KclManager {
|
2023-10-11 13:36:54 +11:00
|
|
|
private _ast: Program = {
|
|
|
|
body: [],
|
|
|
|
start: 0,
|
|
|
|
end: 0,
|
|
|
|
nonCodeMeta: {
|
|
|
|
nonCodeNodes: {},
|
New parser built in Winnow (#731)
* New parser built with Winnow
This new parser uses [winnow](docs.rs/winnow) to replace the handwritten recursive parser.
## Differences
I think the Winnow parser is more readable than handwritten one, due to reusing standard combinators. If you have a parsre like `p` or `q` you can combine them with standard functions like `repeat(0..4, p)`, `opt(p)`, `alt((p, q))` and `separated1(p, ", ")`. This IMO makes it more readable once you know what those standard functions do.
It's also more accurate now -- e.g. the parser no longer swallows whitespace between comments, or inserts it where there was none before. It no longer changes // comments to /* comments depending on the surrounding whitespace.
Primary form of testing was running the same KCL program through both the old and new parsers and asserting that both parsers produce the same AST. See the test `parser::parser_impl::tests::check_parsers_work_the_same`. But occasionally the new and old parsers disagree. This is either:
- Innocuous (e.g. disagreeing on whether a comment starts at the preceding whitespace or at the //)
- Helpful (e.g. new parser recognizes comments more accurately, preserving the difference between // and /* comments)
- Acceptably bad (e.g. new parser sometimes outputs worse error messages, TODO in #784)
so those KCL programs have their own unit tests in `parser_impl.rs` demonstrating the behaviour.
If you'd like to review this PR, it's arguably more important to review changes to the existing unit tests rather than the new parser itself. Because changes to the unit tests show where my parser changes behaviour -- usually for the better, occasionally for the worse (e.g. a worse error message than before). I think overall the improvements are worth it that I'd like to merge it without spending another week fixing it up -- we can fix the error messages in a follow-up PR.
## Performance
| Benchmark | Old parser (this branch) | New parser (this branch) | Speedup |
| ------------- | ------------- | ------------- | ------------- |
| Pipes on pipes | 922 ms | 42 ms | 21x |
| Kitt SVG | 148 ms | 7 ms | 21x |
There's definitely still room to improve performance:
- https://github.com/KittyCAD/modeling-app/issues/839
- https://github.com/KittyCAD/modeling-app/issues/840
## Winnow
Y'all know I love [Nom](docs.rs/nom) and I've blogged about it a lot. But I'm very happy using Winnow, a fork. It's got some really nice usability improvements. While writing this PR I found some bugs or unclear docs in Winnow:
- https://github.com/winnow-rs/winnow/issues/339
- https://github.com/winnow-rs/winnow/issues/341
- https://github.com/winnow-rs/winnow/issues/342
- https://github.com/winnow-rs/winnow/issues/344
The maintainer was quick to close them and release new versions within a few hours, so I feel very confident building the parser on this library. It's a clear improvement over Nom and it's used in toml-edit (and therefore within Cargo) and Gitoxide, so it's becoming a staple of the Rust ecosystem, which adds confidence.
Closes #716
Closes #815
Closes #599
2023-10-12 09:42:37 -05:00
|
|
|
start: [],
|
2024-07-09 12:24:42 -04:00
|
|
|
digest: null,
|
2023-10-11 13:36:54 +11:00
|
|
|
},
|
2024-07-09 12:24:42 -04:00
|
|
|
digest: null,
|
2023-10-11 13:36:54 +11:00
|
|
|
}
|
2024-07-22 19:43:40 -04:00
|
|
|
private _programMemory: ProgramMemory = ProgramMemory.empty()
|
2024-10-02 13:15:40 +10:00
|
|
|
lastSuccessfulProgramMemory: ProgramMemory = ProgramMemory.empty()
|
2023-10-11 13:36:54 +11:00
|
|
|
private _logs: string[] = []
|
2024-07-29 08:41:02 -07:00
|
|
|
private _lints: Diagnostic[] = []
|
2023-10-11 13:36:54 +11:00
|
|
|
private _kclErrors: KCLError[] = []
|
|
|
|
private _isExecuting = false
|
2024-08-14 08:49:00 -07:00
|
|
|
private _executeIsStale: ExecuteArgs | null = null
|
2023-10-17 12:07:48 +11:00
|
|
|
private _wasmInitFailed = true
|
2023-10-11 13:36:54 +11:00
|
|
|
|
|
|
|
engineCommandManager: EngineCommandManager
|
|
|
|
|
2023-10-17 12:07:48 +11:00
|
|
|
private _isExecutingCallback: (arg: boolean) => void = () => {}
|
2023-10-11 13:36:54 +11:00
|
|
|
private _astCallBack: (arg: Program) => void = () => {}
|
|
|
|
private _programMemoryCallBack: (arg: ProgramMemory) => void = () => {}
|
|
|
|
private _logsCallBack: (arg: string[]) => void = () => {}
|
|
|
|
private _kclErrorsCallBack: (arg: KCLError[]) => void = () => {}
|
2023-10-17 12:07:48 +11:00
|
|
|
private _wasmInitFailedCallback: (arg: boolean) => void = () => {}
|
2023-10-18 08:03:02 +11:00
|
|
|
private _executeCallback: () => void = () => {}
|
2023-10-11 13:36:54 +11:00
|
|
|
|
|
|
|
get ast() {
|
|
|
|
return this._ast
|
|
|
|
}
|
|
|
|
set ast(ast) {
|
|
|
|
this._ast = ast
|
|
|
|
this._astCallBack(ast)
|
|
|
|
}
|
|
|
|
|
|
|
|
get programMemory() {
|
|
|
|
return this._programMemory
|
|
|
|
}
|
|
|
|
set programMemory(programMemory) {
|
|
|
|
this._programMemory = programMemory
|
|
|
|
this._programMemoryCallBack(programMemory)
|
|
|
|
}
|
|
|
|
|
|
|
|
get logs() {
|
|
|
|
return this._logs
|
|
|
|
}
|
|
|
|
set logs(logs) {
|
|
|
|
this._logs = logs
|
|
|
|
this._logsCallBack(logs)
|
|
|
|
}
|
|
|
|
|
2024-07-29 08:41:02 -07:00
|
|
|
get lints() {
|
|
|
|
return this._lints
|
|
|
|
}
|
|
|
|
|
|
|
|
set lints(lints) {
|
|
|
|
if (lints === this._lints) return
|
|
|
|
this._lints = lints
|
|
|
|
// Run the lints through the diagnostics.
|
|
|
|
this.kclErrors = this._kclErrors
|
|
|
|
}
|
|
|
|
|
2023-10-11 13:36:54 +11:00
|
|
|
get kclErrors() {
|
|
|
|
return this._kclErrors
|
|
|
|
}
|
|
|
|
set kclErrors(kclErrors) {
|
2024-07-29 08:41:02 -07:00
|
|
|
if (kclErrors === this._kclErrors && this.lints.length === 0) return
|
2023-10-11 13:36:54 +11:00
|
|
|
this._kclErrors = kclErrors
|
2024-08-04 15:16:34 -07:00
|
|
|
this.setDiagnosticsForCurrentErrors()
|
|
|
|
this._kclErrorsCallBack(kclErrors)
|
|
|
|
}
|
|
|
|
|
|
|
|
setDiagnosticsForCurrentErrors() {
|
|
|
|
let diagnostics = kclErrorsToDiagnostics(this.kclErrors)
|
2024-07-29 08:41:02 -07:00
|
|
|
if (this.lints.length > 0) {
|
|
|
|
diagnostics = diagnostics.concat(this.lints)
|
|
|
|
}
|
|
|
|
editorManager.setDiagnostics(diagnostics)
|
2023-10-11 13:36:54 +11:00
|
|
|
}
|
|
|
|
|
2024-07-29 08:41:02 -07:00
|
|
|
addKclErrors(kclErrors: KCLError[]) {
|
|
|
|
if (kclErrors.length === 0) return
|
|
|
|
this.kclErrors = this.kclErrors.concat(kclErrors)
|
|
|
|
}
|
|
|
|
|
2023-10-11 13:36:54 +11:00
|
|
|
get isExecuting() {
|
|
|
|
return this._isExecuting
|
|
|
|
}
|
2024-08-30 05:14:24 -05:00
|
|
|
|
2023-10-11 13:36:54 +11:00
|
|
|
set isExecuting(isExecuting) {
|
|
|
|
this._isExecuting = isExecuting
|
2024-08-14 08:49:00 -07:00
|
|
|
// If we have finished executing, but the execute is stale, we should
|
|
|
|
// execute again.
|
|
|
|
if (!isExecuting && this.executeIsStale) {
|
|
|
|
const args = this.executeIsStale
|
|
|
|
this.executeIsStale = null
|
2024-09-09 18:17:45 -04:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
2024-08-14 08:49:00 -07:00
|
|
|
this.executeAst(args)
|
|
|
|
}
|
2023-10-11 13:36:54 +11:00
|
|
|
this._isExecutingCallback(isExecuting)
|
|
|
|
}
|
|
|
|
|
2024-08-14 08:49:00 -07:00
|
|
|
get executeIsStale() {
|
|
|
|
return this._executeIsStale
|
|
|
|
}
|
|
|
|
|
|
|
|
set executeIsStale(executeIsStale) {
|
|
|
|
this._executeIsStale = executeIsStale
|
|
|
|
}
|
|
|
|
|
2023-10-17 12:07:48 +11:00
|
|
|
get wasmInitFailed() {
|
|
|
|
return this._wasmInitFailed
|
|
|
|
}
|
|
|
|
set wasmInitFailed(wasmInitFailed) {
|
|
|
|
this._wasmInitFailed = wasmInitFailed
|
|
|
|
this._wasmInitFailedCallback(wasmInitFailed)
|
|
|
|
}
|
|
|
|
|
2023-10-11 13:36:54 +11:00
|
|
|
constructor(engineCommandManager: EngineCommandManager) {
|
|
|
|
this.engineCommandManager = engineCommandManager
|
2023-11-06 11:49:13 +11:00
|
|
|
|
2024-09-09 18:17:45 -04:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
2024-02-11 12:59:00 +11:00
|
|
|
this.ensureWasmInit().then(() => {
|
2024-04-17 20:18:07 -07:00
|
|
|
this.ast = this.safeParse(codeManager.code) || this.ast
|
2024-02-11 12:59:00 +11:00
|
|
|
})
|
2023-10-11 13:36:54 +11:00
|
|
|
}
|
2024-04-17 20:18:07 -07:00
|
|
|
|
2023-10-11 13:36:54 +11:00
|
|
|
registerCallBacks({
|
|
|
|
setProgramMemory,
|
|
|
|
setAst,
|
|
|
|
setLogs,
|
|
|
|
setKclErrors,
|
|
|
|
setIsExecuting,
|
2023-10-17 12:07:48 +11:00
|
|
|
setWasmInitFailed,
|
2023-10-11 13:36:54 +11:00
|
|
|
}: {
|
|
|
|
setProgramMemory: (arg: ProgramMemory) => void
|
|
|
|
setAst: (arg: Program) => void
|
|
|
|
setLogs: (arg: string[]) => void
|
|
|
|
setKclErrors: (arg: KCLError[]) => void
|
|
|
|
setIsExecuting: (arg: boolean) => void
|
2023-10-17 12:07:48 +11:00
|
|
|
setWasmInitFailed: (arg: boolean) => void
|
2023-10-11 13:36:54 +11:00
|
|
|
}) {
|
|
|
|
this._programMemoryCallBack = setProgramMemory
|
|
|
|
this._astCallBack = setAst
|
|
|
|
this._logsCallBack = setLogs
|
|
|
|
this._kclErrorsCallBack = setKclErrors
|
|
|
|
this._isExecutingCallback = setIsExecuting
|
2023-10-17 12:07:48 +11:00
|
|
|
this._wasmInitFailedCallback = setWasmInitFailed
|
|
|
|
}
|
2023-10-18 08:03:02 +11:00
|
|
|
registerExecuteCallback(callback: () => void) {
|
|
|
|
this._executeCallback = callback
|
|
|
|
}
|
2023-10-17 12:07:48 +11:00
|
|
|
|
2024-06-19 16:15:22 -04:00
|
|
|
clearAst() {
|
|
|
|
this._ast = {
|
|
|
|
body: [],
|
|
|
|
start: 0,
|
|
|
|
end: 0,
|
|
|
|
nonCodeMeta: {
|
|
|
|
nonCodeNodes: {},
|
|
|
|
start: [],
|
2024-07-09 12:24:42 -04:00
|
|
|
digest: null,
|
2024-06-19 16:15:22 -04:00
|
|
|
},
|
2024-07-09 12:24:42 -04:00
|
|
|
digest: null,
|
2024-06-19 16:15:22 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-06 17:47:45 +11:00
|
|
|
safeParse(code: string): Program | null {
|
2024-06-24 11:45:40 -04:00
|
|
|
const ast = parse(code)
|
2024-07-29 08:41:02 -07:00
|
|
|
this.lints = []
|
2024-06-24 11:45:40 -04:00
|
|
|
this.kclErrors = []
|
|
|
|
if (!err(ast)) return ast
|
|
|
|
const kclerror: KCLError = ast as KCLError
|
|
|
|
|
2024-07-29 08:41:02 -07:00
|
|
|
this.addKclErrors([kclerror])
|
2024-06-24 11:45:40 -04:00
|
|
|
// TODO: re-eval if session should end?
|
|
|
|
if (kclerror.msg === 'file is empty')
|
|
|
|
this.engineCommandManager?.endSession()
|
|
|
|
return null
|
2023-11-06 17:47:45 +11:00
|
|
|
}
|
|
|
|
|
2023-10-17 12:07:48 +11:00
|
|
|
async ensureWasmInit() {
|
|
|
|
try {
|
|
|
|
await initPromise
|
|
|
|
if (this.wasmInitFailed) {
|
|
|
|
this.wasmInitFailed = false
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
this.wasmInitFailed = true
|
|
|
|
}
|
2023-10-11 13:36:54 +11:00
|
|
|
}
|
|
|
|
|
2024-02-19 09:23:18 +11:00
|
|
|
private _cancelTokens: Map<number, boolean> = new Map()
|
|
|
|
|
2024-04-17 20:18:07 -07:00
|
|
|
// This NEVER updates the code, if you want to update the code DO NOT add to
|
|
|
|
// this function, too many other things that don't want it exist.
|
|
|
|
// just call to codeManager from wherever you want in other files.
|
2024-08-14 08:49:00 -07:00
|
|
|
async executeAst(args: ExecuteArgs = {}): Promise<void> {
|
|
|
|
if (this.isExecuting) {
|
|
|
|
this.executeIsStale = args
|
2024-08-30 05:14:24 -05:00
|
|
|
|
|
|
|
// The previous execteAst will be rejected and cleaned up. The execution will be marked as stale.
|
|
|
|
// A new executeAst will start.
|
|
|
|
this.engineCommandManager.rejectAllModelingCommands(
|
|
|
|
EXECUTE_AST_INTERRUPT_ERROR_MESSAGE
|
|
|
|
)
|
2024-08-14 08:49:00 -07:00
|
|
|
// Exit early if we are already executing.
|
|
|
|
return
|
2024-07-04 01:19:24 -04:00
|
|
|
}
|
2024-08-14 08:49:00 -07:00
|
|
|
|
|
|
|
const ast = args.ast || this.ast
|
|
|
|
|
|
|
|
const currentExecutionId = args.executionId || Date.now()
|
2024-02-19 09:23:18 +11:00
|
|
|
this._cancelTokens.set(currentExecutionId, false)
|
|
|
|
|
2023-10-11 13:36:54 +11:00
|
|
|
this.isExecuting = true
|
2024-07-29 19:55:53 -07:00
|
|
|
// Make sure we clear before starting again. End session will do this.
|
|
|
|
this.engineCommandManager?.endSession()
|
2024-02-26 21:02:33 +11:00
|
|
|
await this.ensureWasmInit()
|
2024-08-30 05:14:24 -05:00
|
|
|
const { logs, errors, programMemory, isInterrupted } = await executeAst({
|
2023-10-11 13:36:54 +11:00
|
|
|
ast,
|
|
|
|
engineCommandManager: this.engineCommandManager,
|
|
|
|
})
|
2024-06-26 00:04:52 -04:00
|
|
|
|
2024-08-30 05:14:24 -05:00
|
|
|
// Program was not interrupted, setup the scene
|
|
|
|
// Do not send send scene commands if the program was interrupted, go to clean up
|
|
|
|
if (!isInterrupted) {
|
|
|
|
this.lints = await lintAst({ ast: ast })
|
|
|
|
|
|
|
|
sceneInfra.modelingSend({ type: 'code edit during sketch' })
|
|
|
|
defaultSelectionFilter(programMemory, this.engineCommandManager)
|
|
|
|
|
|
|
|
if (args.zoomToFit) {
|
|
|
|
let zoomObjectId: string | undefined = ''
|
|
|
|
if (args.zoomOnRangeAndType) {
|
|
|
|
zoomObjectId = this.engineCommandManager?.mapRangeToObjectId(
|
|
|
|
args.zoomOnRangeAndType.range,
|
|
|
|
args.zoomOnRangeAndType.type
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
await this.engineCommandManager.sendSceneCommand({
|
|
|
|
type: 'modeling_cmd_req',
|
|
|
|
cmd_id: uuidv4(),
|
|
|
|
cmd: {
|
|
|
|
type: 'zoom_to_fit',
|
|
|
|
object_ids: zoomObjectId ? [zoomObjectId] : [], // leave empty to zoom to all objects
|
|
|
|
padding: 0.1, // padding around the objects
|
2024-10-04 13:47:44 -07:00
|
|
|
animated: false, // don't animate the zoom for now
|
2024-08-30 05:14:24 -05:00
|
|
|
},
|
|
|
|
})
|
2024-07-04 01:19:24 -04:00
|
|
|
}
|
2024-05-24 12:32:15 -07:00
|
|
|
}
|
|
|
|
|
2023-10-11 13:36:54 +11:00
|
|
|
this.isExecuting = false
|
2024-08-14 08:49:00 -07:00
|
|
|
|
2024-02-19 09:23:18 +11:00
|
|
|
// Check the cancellation token for this execution before applying side effects
|
|
|
|
if (this._cancelTokens.get(currentExecutionId)) {
|
|
|
|
this._cancelTokens.delete(currentExecutionId)
|
|
|
|
return
|
|
|
|
}
|
2024-10-09 10:33:20 -04:00
|
|
|
|
|
|
|
// Exit sketch mode if the AST is empty
|
|
|
|
if (this._isAstEmpty(ast)) {
|
|
|
|
await this.disableSketchMode()
|
|
|
|
}
|
|
|
|
|
2023-11-08 12:27:43 +11:00
|
|
|
this.logs = logs
|
2024-08-30 05:14:24 -05:00
|
|
|
// Do not add the errors since the program was interrupted and the error is not a real KCL error
|
|
|
|
this.addKclErrors(isInterrupted ? [] : errors)
|
2023-11-08 12:27:43 +11:00
|
|
|
this.programMemory = programMemory
|
2024-10-02 13:15:40 +10:00
|
|
|
if (!errors.length) {
|
|
|
|
this.lastSuccessfulProgramMemory = programMemory
|
|
|
|
}
|
2023-11-08 12:27:43 +11:00
|
|
|
this.ast = { ...ast }
|
2023-10-18 08:03:02 +11:00
|
|
|
this._executeCallback()
|
2024-03-22 16:55:30 +11:00
|
|
|
this.engineCommandManager.addCommandLog({
|
2023-11-24 08:59:24 +11:00
|
|
|
type: 'execution-done',
|
|
|
|
data: null,
|
|
|
|
})
|
2024-08-30 05:14:24 -05:00
|
|
|
|
2024-02-19 09:23:18 +11:00
|
|
|
this._cancelTokens.delete(currentExecutionId)
|
2023-10-11 13:36:54 +11:00
|
|
|
}
|
2024-04-17 20:18:07 -07:00
|
|
|
// NOTE: this always updates the code state and editor.
|
|
|
|
// DO NOT CALL THIS from codemirror ever.
|
2024-02-11 12:59:00 +11:00
|
|
|
async executeAstMock(
|
|
|
|
ast: Program = this._ast,
|
|
|
|
{
|
|
|
|
updates,
|
|
|
|
}: {
|
2024-04-17 20:18:07 -07:00
|
|
|
updates: 'none' | 'artifactRanges'
|
2024-02-11 12:59:00 +11:00
|
|
|
} = { updates: 'none' }
|
|
|
|
) {
|
2023-10-17 12:07:48 +11:00
|
|
|
await this.ensureWasmInit()
|
2024-06-11 19:23:35 -04:00
|
|
|
|
2023-10-11 13:36:54 +11:00
|
|
|
const newCode = recast(ast)
|
2024-06-24 11:45:40 -04:00
|
|
|
if (err(newCode)) {
|
|
|
|
console.error(newCode)
|
|
|
|
return
|
|
|
|
}
|
2023-11-06 17:47:45 +11:00
|
|
|
const newAst = this.safeParse(newCode)
|
2024-06-26 00:04:52 -04:00
|
|
|
if (!newAst) {
|
|
|
|
this.clearAst()
|
|
|
|
return
|
|
|
|
}
|
2024-05-06 19:28:30 +10:00
|
|
|
codeManager.updateCodeEditor(newCode)
|
2024-04-17 20:18:07 -07:00
|
|
|
// Write the file to disk.
|
|
|
|
await codeManager.writeToFile()
|
2023-10-11 13:36:54 +11:00
|
|
|
this._ast = { ...newAst }
|
|
|
|
|
|
|
|
const { logs, errors, programMemory } = await executeAst({
|
|
|
|
ast: newAst,
|
|
|
|
engineCommandManager: this.engineCommandManager,
|
|
|
|
useFakeExecutor: true,
|
|
|
|
})
|
2024-06-26 00:04:52 -04:00
|
|
|
|
2023-10-11 13:36:54 +11:00
|
|
|
this._logs = logs
|
|
|
|
this._kclErrors = errors
|
|
|
|
this._programMemory = programMemory
|
2024-10-02 13:15:40 +10:00
|
|
|
if (!errors.length) {
|
|
|
|
this.lastSuccessfulProgramMemory = programMemory
|
|
|
|
}
|
2024-04-17 20:18:07 -07:00
|
|
|
if (updates !== 'artifactRanges') return
|
2024-08-03 18:08:51 +10:00
|
|
|
|
|
|
|
// TODO the below seems like a work around, I wish there's a comment explaining exactly what
|
|
|
|
// problem this solves, but either way we should strive to remove it.
|
|
|
|
Array.from(this.engineCommandManager.artifactGraph).forEach(
|
2024-02-11 12:59:00 +11:00
|
|
|
([commandId, artifact]) => {
|
2024-08-03 18:08:51 +10:00
|
|
|
if (!('codeRef' in artifact)) return
|
2024-06-24 11:45:40 -04:00
|
|
|
const _node1 = getNodeFromPath<CallExpression>(
|
2024-03-22 16:55:30 +11:00
|
|
|
this.ast,
|
2024-08-03 18:08:51 +10:00
|
|
|
artifact.codeRef.pathToNode,
|
2024-02-11 12:59:00 +11:00
|
|
|
'CallExpression'
|
2024-06-24 11:45:40 -04:00
|
|
|
)
|
|
|
|
if (err(_node1)) return
|
|
|
|
const { node } = _node1
|
2024-02-11 12:59:00 +11:00
|
|
|
if (node.type !== 'CallExpression') return
|
2024-08-03 18:08:51 +10:00
|
|
|
const [oldStart, oldEnd] = artifact.codeRef.range
|
2024-02-11 12:59:00 +11:00
|
|
|
if (oldStart === 0 && oldEnd === 0) return
|
|
|
|
if (oldStart === node.start && oldEnd === node.end) return
|
2024-08-03 18:08:51 +10:00
|
|
|
this.engineCommandManager.artifactGraph.set(commandId, {
|
|
|
|
...artifact,
|
|
|
|
codeRef: {
|
|
|
|
...artifact.codeRef,
|
|
|
|
range: [node.start, node.end],
|
|
|
|
},
|
|
|
|
})
|
2024-02-11 12:59:00 +11:00
|
|
|
}
|
|
|
|
)
|
2023-10-11 13:36:54 +11:00
|
|
|
}
|
2024-02-19 09:23:18 +11:00
|
|
|
cancelAllExecutions() {
|
|
|
|
this._cancelTokens.forEach((_, key) => {
|
|
|
|
this._cancelTokens.set(key, true)
|
|
|
|
})
|
2023-10-11 13:36:54 +11:00
|
|
|
}
|
2024-07-23 20:37:04 -04:00
|
|
|
async executeCode(zoomToFit?: boolean): Promise<void> {
|
2024-04-18 12:40:05 -07:00
|
|
|
const ast = this.safeParse(codeManager.code)
|
2024-06-19 16:15:22 -04:00
|
|
|
if (!ast) {
|
|
|
|
this.clearAst()
|
|
|
|
return
|
|
|
|
}
|
2024-04-18 12:40:05 -07:00
|
|
|
this.ast = { ...ast }
|
2024-08-14 08:49:00 -07:00
|
|
|
return this.executeAst({ zoomToFit })
|
2023-10-11 13:36:54 +11:00
|
|
|
}
|
|
|
|
format() {
|
2024-04-17 20:18:07 -07:00
|
|
|
const originalCode = codeManager.code
|
|
|
|
const ast = this.safeParse(originalCode)
|
2024-06-19 16:15:22 -04:00
|
|
|
if (!ast) {
|
|
|
|
this.clearAst()
|
|
|
|
return
|
|
|
|
}
|
2024-04-17 20:18:07 -07:00
|
|
|
const code = recast(ast)
|
2024-06-24 11:45:40 -04:00
|
|
|
if (err(code)) {
|
|
|
|
console.error(code)
|
|
|
|
return
|
|
|
|
}
|
2024-04-17 20:18:07 -07:00
|
|
|
if (originalCode === code) return
|
|
|
|
|
|
|
|
// Update the code state and the editor.
|
|
|
|
codeManager.updateCodeStateEditor(code)
|
|
|
|
// Write back to the file system.
|
2024-09-09 18:17:45 -04:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
2024-04-17 20:18:07 -07:00
|
|
|
codeManager.writeToFile()
|
2024-08-25 15:14:38 -07:00
|
|
|
|
|
|
|
// execute the code.
|
2024-09-09 18:17:45 -04:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
2024-08-25 15:14:38 -07:00
|
|
|
this.executeCode()
|
2023-10-11 13:36:54 +11:00
|
|
|
}
|
2023-11-01 17:34:54 -05:00
|
|
|
// There's overlapping responsibility between updateAst and executeAst.
|
2023-10-11 13:36:54 +11:00
|
|
|
// updateAst was added as it was used a lot before xState migration so makes the port easier.
|
|
|
|
// but should probably have think about which of the function to keep
|
2024-04-17 20:18:07 -07:00
|
|
|
// This always updates the code state and editor and writes to the file system.
|
2023-10-11 13:36:54 +11:00
|
|
|
async updateAst(
|
|
|
|
ast: Program,
|
|
|
|
execute: boolean,
|
|
|
|
optionalParams?: {
|
2024-09-23 08:07:31 +02:00
|
|
|
focusPath?: Array<PathToNode>
|
2024-07-04 01:19:24 -04:00
|
|
|
zoomToFit?: boolean
|
|
|
|
zoomOnRangeAndType?: {
|
|
|
|
range: SourceRange
|
|
|
|
type: string
|
|
|
|
}
|
2023-10-11 13:36:54 +11:00
|
|
|
}
|
2024-06-24 11:45:40 -04:00
|
|
|
): Promise<{
|
|
|
|
newAst: Program
|
|
|
|
selections?: Selections
|
|
|
|
}> {
|
2023-10-11 13:36:54 +11:00
|
|
|
const newCode = recast(ast)
|
2024-06-24 11:45:40 -04:00
|
|
|
if (err(newCode)) return Promise.reject(newCode)
|
|
|
|
|
2023-11-06 17:47:45 +11:00
|
|
|
const astWithUpdatedSource = this.safeParse(newCode)
|
2024-06-24 11:45:40 -04:00
|
|
|
if (!astWithUpdatedSource) return Promise.reject(new Error('bad ast'))
|
|
|
|
let returnVal: Selections | undefined = undefined
|
2023-10-11 13:36:54 +11:00
|
|
|
|
|
|
|
if (optionalParams?.focusPath) {
|
|
|
|
returnVal = {
|
2024-09-23 08:07:31 +02:00
|
|
|
codeBasedSelections: [],
|
|
|
|
otherSelections: [],
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const path of optionalParams.focusPath) {
|
|
|
|
const getNodeFromPathResult = getNodeFromPath<any>(
|
|
|
|
astWithUpdatedSource,
|
|
|
|
path
|
|
|
|
)
|
|
|
|
if (err(getNodeFromPathResult))
|
|
|
|
return Promise.reject(getNodeFromPathResult)
|
|
|
|
const { node } = getNodeFromPathResult
|
|
|
|
|
|
|
|
const { start, end } = node
|
|
|
|
|
|
|
|
if (!start || !end)
|
|
|
|
return {
|
|
|
|
selections: undefined,
|
|
|
|
newAst: astWithUpdatedSource,
|
|
|
|
}
|
|
|
|
|
|
|
|
if (start && end) {
|
|
|
|
returnVal.codeBasedSelections.push({
|
2023-10-11 13:36:54 +11:00
|
|
|
type: 'default',
|
|
|
|
range: [start, end],
|
2024-09-23 08:07:31 +02:00
|
|
|
})
|
|
|
|
}
|
2023-10-11 13:36:54 +11:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (execute) {
|
|
|
|
// Call execute on the set ast.
|
2024-04-17 20:18:07 -07:00
|
|
|
// Update the code state and editor.
|
2024-05-06 19:28:30 +10:00
|
|
|
codeManager.updateCodeEditor(newCode)
|
2024-04-17 20:18:07 -07:00
|
|
|
// Write the file to disk.
|
|
|
|
await codeManager.writeToFile()
|
2024-08-14 08:49:00 -07:00
|
|
|
await this.executeAst({
|
|
|
|
ast: astWithUpdatedSource,
|
|
|
|
zoomToFit: optionalParams?.zoomToFit,
|
|
|
|
zoomOnRangeAndType: optionalParams?.zoomOnRangeAndType,
|
|
|
|
})
|
2023-10-11 13:36:54 +11:00
|
|
|
} else {
|
|
|
|
// When we don't re-execute, we still want to update the program
|
|
|
|
// memory with the new ast. So we will hit the mock executor
|
2024-04-17 20:18:07 -07:00
|
|
|
// instead..
|
|
|
|
// Execute ast mock will update the code state and editor.
|
|
|
|
await this.executeAstMock(astWithUpdatedSource)
|
2023-10-11 13:36:54 +11:00
|
|
|
}
|
2024-06-24 11:45:40 -04:00
|
|
|
|
|
|
|
return { selections: returnVal, newAst: astWithUpdatedSource }
|
2023-10-11 13:36:54 +11:00
|
|
|
}
|
2024-02-17 07:04:24 +11:00
|
|
|
|
|
|
|
get defaultPlanes() {
|
|
|
|
return this?.engineCommandManager?.defaultPlanes
|
|
|
|
}
|
|
|
|
|
2024-06-18 16:08:41 +10:00
|
|
|
showPlanes(all = false) {
|
|
|
|
if (!this.defaultPlanes) return Promise.all([])
|
|
|
|
const thePromises = [
|
|
|
|
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xy, false),
|
|
|
|
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, false),
|
|
|
|
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, false),
|
|
|
|
]
|
|
|
|
if (all) {
|
|
|
|
thePromises.push(
|
|
|
|
this.engineCommandManager.setPlaneHidden(
|
|
|
|
this.defaultPlanes.negXy,
|
|
|
|
false
|
|
|
|
)
|
|
|
|
)
|
|
|
|
thePromises.push(
|
|
|
|
this.engineCommandManager.setPlaneHidden(
|
|
|
|
this.defaultPlanes.negYz,
|
|
|
|
false
|
|
|
|
)
|
|
|
|
)
|
|
|
|
thePromises.push(
|
|
|
|
this.engineCommandManager.setPlaneHidden(
|
|
|
|
this.defaultPlanes.negXz,
|
|
|
|
false
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
return Promise.all(thePromises)
|
2024-02-17 07:04:24 +11:00
|
|
|
}
|
|
|
|
|
2024-06-18 16:08:41 +10:00
|
|
|
hidePlanes(all = false) {
|
|
|
|
if (!this.defaultPlanes) return Promise.all([])
|
|
|
|
const thePromises = [
|
|
|
|
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xy, true),
|
|
|
|
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, true),
|
|
|
|
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, true),
|
|
|
|
]
|
|
|
|
if (all) {
|
|
|
|
thePromises.push(
|
|
|
|
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.negXy, true)
|
|
|
|
)
|
|
|
|
thePromises.push(
|
|
|
|
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.negYz, true)
|
|
|
|
)
|
|
|
|
thePromises.push(
|
|
|
|
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.negXz, true)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
return Promise.all(thePromises)
|
2024-02-17 07:04:24 +11:00
|
|
|
}
|
2024-05-21 05:55:34 +10:00
|
|
|
defaultSelectionFilter() {
|
|
|
|
defaultSelectionFilter(this.programMemory, this.engineCommandManager)
|
|
|
|
}
|
2024-10-09 10:33:20 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* We can send a single command of 'enable_sketch_mode' or send this in a batched request.
|
|
|
|
* When there is no code in the KCL editor we should be sending 'sketch_mode_disable' since any previous half finished
|
|
|
|
* code could leave the state of the application in sketch mode on the engine side.
|
|
|
|
*/
|
|
|
|
async disableSketchMode() {
|
|
|
|
await this.engineCommandManager.sendSceneCommand({
|
|
|
|
type: 'modeling_cmd_req',
|
|
|
|
cmd_id: uuidv4(),
|
|
|
|
cmd: { type: 'sketch_mode_disable' },
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Determines if there is no KCL code which means it is executing a blank KCL file
|
|
|
|
_isAstEmpty(ast: Program) {
|
|
|
|
return ast.start === 0 && ast.end === 0 && ast.body.length === 0
|
|
|
|
}
|
2023-10-11 13:36:54 +11:00
|
|
|
}
|
|
|
|
|
2024-05-21 05:55:34 +10:00
|
|
|
function defaultSelectionFilter(
|
2024-03-22 16:55:30 +11:00
|
|
|
programMemory: ProgramMemory,
|
|
|
|
engineCommandManager: EngineCommandManager
|
|
|
|
) {
|
2024-09-09 18:17:45 -04:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
2024-09-27 15:44:44 -07:00
|
|
|
programMemory.hasSketchOrSolid() &&
|
2024-03-22 10:23:04 +11:00
|
|
|
engineCommandManager.sendSceneCommand({
|
2024-06-06 16:03:10 +10:00
|
|
|
type: 'modeling_cmd_req',
|
|
|
|
cmd_id: uuidv4(),
|
|
|
|
cmd: {
|
|
|
|
type: 'set_selection_filter',
|
|
|
|
filter: ['face', 'edge', 'solid2d', 'curve'],
|
|
|
|
},
|
2024-03-22 10:23:04 +11:00
|
|
|
})
|
|
|
|
}
|