Codemirror lsp enhance (#6580)

* codemirror side

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* codemirror actions

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* codemirror actions

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* code mirror now shows lint suggestions

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix hanging params with test

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates for signature help

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix clone

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* clippy

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* clippy

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* Update packages/codemirror-lsp-client/src/plugin/lsp.ts

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

* z-index

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* playwright tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
This commit is contained in:
Jess Frazelle
2025-04-29 17:57:02 -07:00
committed by GitHub
parent 844f229b5a
commit 29b8a442c2
35 changed files with 6746 additions and 80 deletions

View File

@ -196,6 +196,68 @@ sketch001 = startSketchOn(XY)
)
})
test('F2 can rename a variable', async ({ page, homePage, scene }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`myVARName = 100
a1 = startSketchOn(offsetPlane(XY, offset = 10))
|> startProfile(at = [0, 0])
|> line(end = [myVARName, 0])
|> yLine(length = -100.0)
|> xLine(length = -100.0)
|> yLine(length = 100.0)
|> close()
|> extrude(length = 12)`
)
})
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.connectionEstablished()
// check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
await u.codeLocator.click()
// Move the cursor to the start of the code
await page.keyboard.press('ControlOrMeta+Home')
await page.keyboard.press('ArrowRight')
await page.waitForTimeout(100)
// Press F2 to rename the variable
await page.keyboard.press('F2')
// Wait for the rename box.
await expect(page.locator('.cm-rename-popup')).toBeVisible()
// Make sure we are focused on the rename box
await expect(page.locator('.cm-rename-popup input')).toBeFocused()
// Type the new name
await page.keyboard.type('myNewName')
// Press Enter to accept the rename
await page.keyboard.press('Enter')
// Ensure we have the new name
await expect(page.locator('.cm-content')).toHaveText(
`myNewName = 100
a1 = startSketchOn(offsetPlane(XY, offset = 10))
|> startProfile(at = [0, 0])
|> line(end = [myNewName, 0])
|> yLine(length = -100.0)
|> xLine(length = -100.0)
|> yLine(length = 100.0)
|> close()
|> extrude(length = 12)`.replaceAll('\n', '')
)
})
test('if you click the format button it formats your code and executes so lints are still there', async ({
page,
homePage,
@ -481,6 +543,113 @@ sketch_001 = startSketchOn(XY)
).toBeVisible()
})
test('you can accept the suggestion from a lint', async ({
page,
homePage,
scene,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`a1 = startSketchOn({
origin = { x = 0, y = 0, z = 0 },
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 12, z = 0 },
})
|> startProfile(at = [0, 0])
|> line(end = [100.0, 0])
|> yLine(length = -100.0)
|> xLine(length = -100.0)
|> yLine(length = 100.0)
|> close()
|> extrude(length = 3.14)`
)
})
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.connectionEstablished()
await expect(page.locator('.cm-lint-marker-info')).toBeVisible()
// error in guter
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-info')
await expect(
page.getByText('offsetPlane should be used').first()
).toBeVisible()
// select the line that's causing the error and delete it
// accept the change
await page.getByText('use offsetPlane instead').click()
// Ensure we have the new code
await expect(page.locator('.cm-content')).toHaveText(
`a1 = startSketchOn(offsetPlane(XY, offset = 12))
|> startProfile(at = [0, 0])
|> line(end = [100.0, 0])
|> yLine(length = -100.0)
|> xLine(length = -100.0)
|> yLine(length = 100.0)
|> close()
|> extrude(length = 3.14)`.replaceAll('\n', '')
)
})
test('signature help triggered by comma', async ({
page,
homePage,
scene,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`myVARName = 100
a1 = startSketchOn(offsetPlane(XY, offset = 10))
|> startProfile(at = [0, 0])
|> line(end = [myVARName, 0])
|> yLine(length = -100.0)
|> xLine(length = -100.0)
|> yLine(length = 100.0)
|> close()
|> extrude(length = 12`
)
})
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.connectionEstablished()
// Expect the signature help to NOT be visible
await expect(page.locator('.cm-signature-tooltip')).not.toBeVisible()
// Click in the editor
await page.locator('.cm-content').click()
// Go to the end of the code
await page.keyboard.press('ControlOrMeta+End')
// Type a comma
await page.keyboard.press(',')
// Wait for the signature help to show
await expect(page.locator('.cm-signature-tooltip')).toBeVisible()
// Make sure the parameters are correct
await expect(page.locator('.cm-signature-tooltip')).toContainText(
'sketches:'
)
// Make sure the tooltip goes away after a timeout.
await page.waitForTimeout(12000)
await expect(page.locator('.cm-signature-tooltip')).not.toBeVisible()
})
test('if you write kcl with lint errors you get lints', async ({
page,
homePage,

View File

@ -14,6 +14,7 @@ interface LSPRequestMap {
LSP.CompletionParams,
LSP.CompletionItem[] | LSP.CompletionList | null,
]
'completionItem/resolve': [LSP.CompletionItem, LSP.CompletionItem]
'textDocument/semanticTokens/full': [
LSP.SemanticTokensParams,
LSP.SemanticTokens,
@ -23,6 +24,23 @@ interface LSPRequestMap {
LSP.TextEdit[] | null,
]
'textDocument/foldingRange': [LSP.FoldingRangeParams, LSP.FoldingRange[]]
'textDocument/signatureHelp': [
LSP.SignatureHelpParams,
LSP.SignatureHelp | null,
]
'textDocument/codeAction': [
LSP.CodeActionParams,
(LSP.Command | LSP.CodeAction)[] | null,
]
'textDocument/rename': [LSP.RenameParams, LSP.WorkspaceEdit | null]
'textDocument/prepareRename': [
LSP.PrepareRenameParams,
LSP.Range | LSP.PrepareRenameResult | null,
]
'textDocument/definition': [
LSP.DefinitionParams,
LSP.Definition | LSP.DefinitionLink[] | null,
]
}
// Client to server
@ -124,7 +142,7 @@ export class LanguageServerClient {
async textDocumentSemanticTokensFull(params: LSP.SemanticTokensParams) {
const serverCapabilities = this.getServerCapabilities()
if (!serverCapabilities.semanticTokensProvider) {
return
return null
}
return this.request('textDocument/semanticTokens/full', params)
@ -133,7 +151,7 @@ export class LanguageServerClient {
async textDocumentHover(params: LSP.HoverParams) {
const serverCapabilities = this.getServerCapabilities()
if (!serverCapabilities.hoverProvider) {
return
return null
}
return await this.request('textDocument/hover', params)
}
@ -141,7 +159,7 @@ export class LanguageServerClient {
async textDocumentFormatting(params: LSP.DocumentFormattingParams) {
const serverCapabilities = this.getServerCapabilities()
if (!serverCapabilities.documentFormattingProvider) {
return
return null
}
return await this.request('textDocument/formatting', params)
}
@ -149,7 +167,7 @@ export class LanguageServerClient {
async textDocumentFoldingRange(params: LSP.FoldingRangeParams) {
const serverCapabilities = this.getServerCapabilities()
if (!serverCapabilities.foldingRangeProvider) {
return
return null
}
return await this.request('textDocument/foldingRange', params)
}
@ -157,10 +175,58 @@ export class LanguageServerClient {
async textDocumentCompletion(params: LSP.CompletionParams) {
const serverCapabilities = this.getServerCapabilities()
if (!serverCapabilities.completionProvider) {
return
return null
}
const response = await this.request('textDocument/completion', params)
return response
return await this.request('textDocument/completion', params)
}
async completionItemResolve(params: LSP.CompletionItem) {
const serverCapabilities = this.getServerCapabilities()
if (!serverCapabilities.completionProvider) {
return null
}
return await this.request('completionItem/resolve', params)
}
async textDocumentSignatureHelp(params: LSP.SignatureHelpParams) {
const serverCapabilities = this.getServerCapabilities()
if (!serverCapabilities.signatureHelpProvider) {
return null
}
return await this.request('textDocument/signatureHelp', params)
}
async textDocumentCodeAction(params: LSP.CodeActionParams) {
const serverCapabilities = this.getServerCapabilities()
if (!serverCapabilities.codeActionProvider) {
return null
}
return await this.request('textDocument/codeAction', params)
}
async textDocumentRename(params: LSP.RenameParams) {
const serverCapabilities = this.getServerCapabilities()
if (!serverCapabilities.renameProvider) {
return null
}
return await this.request('textDocument/rename', params)
}
async textDocumentPrepareRename(params: LSP.PrepareRenameParams) {
const serverCapabilities = this.getServerCapabilities()
if (!serverCapabilities.renameProvider) {
return null
}
return await this.request('textDocument/prepareRename', params)
}
async textDocumentDefinition(params: LSP.DefinitionParams) {
const serverCapabilities = this.getServerCapabilities()
if (!serverCapabilities.definitionProvider) {
return null
}
return await this.request('textDocument/definition', params)
}
attachPlugin(plugin: LanguageServerPlugin) {

View File

@ -18,7 +18,9 @@ export { Codec } from './client/codec/utils'
export {
lspDiagnosticsEvent,
lspFormatCodeEvent,
lspRenameEvent,
lspSemanticTokensEvent,
lspCodeActionEvent,
} from './plugin/annotation'
export {
LanguageServerPlugin,

View File

@ -4,9 +4,13 @@ export enum LspAnnotation {
SemanticTokens = 'semantic-tokens',
FormatCode = 'format-code',
Diagnostics = 'diagnostics',
Rename = 'rename',
CodeAction = 'code-action',
}
const lspEvent = Annotation.define<LspAnnotation>()
export const lspSemanticTokensEvent = lspEvent.of(LspAnnotation.SemanticTokens)
export const lspFormatCodeEvent = lspEvent.of(LspAnnotation.FormatCode)
export const lspDiagnosticsEvent = lspEvent.of(LspAnnotation.Diagnostics)
export const lspRenameEvent = lspEvent.of(LspAnnotation.Rename)
export const lspCodeActionEvent = lspEvent.of(LspAnnotation.CodeAction)

View File

@ -9,6 +9,7 @@ import {
prevSnippetField,
startCompletion,
} from '@codemirror/autocomplete'
import type { CompletionContext } from '@codemirror/autocomplete'
import { syntaxTree } from '@codemirror/language'
import type { Extension } from '@codemirror/state'
import { Prec } from '@codemirror/state'
@ -66,7 +67,7 @@ export default function lspAutocompleteExt(
defaultKeymap: false,
override: [
async (context) => {
const { state, pos, explicit, view } = context
const { state, pos, view } = context
let value = view?.plugin(plugin)
if (!value) return null
@ -77,37 +78,55 @@ export default function lspAutocompleteExt(
)
return null
const line = state.doc.lineAt(pos)
let trigKind: CompletionTriggerKind = CompletionTriggerKind.Invoked
let trigChar: string | undefined
if (
!explicit &&
value.client
.getServerCapabilities()
.completionProvider?.triggerCharacters?.includes(
line.text[pos - line.from - 1]
const cmpTriggers = getCompletionTriggerKind(
context,
value.client.getServerCapabilities().completionProvider
?.triggerCharacters ?? []
)
) {
trigKind = CompletionTriggerKind.TriggerCharacter
trigChar = line.text[pos - line.from - 1]
}
if (
trigKind === CompletionTriggerKind.Invoked &&
!context.matchBefore(/\w+$/)
) {
return null
}
if (!cmpTriggers) return null
return await value.requestCompletion(
context,
offsetToPos(state.doc, pos),
{
triggerKind: trigKind,
triggerCharacter: trigChar,
}
cmpTriggers
)
},
],
}),
]
}
export function getCompletionTriggerKind(
context: CompletionContext,
triggerCharacters: string[],
matchBeforePattern?: RegExp
): {
triggerKind: CompletionTriggerKind
triggerCharacter?: string
} | null {
const { state, pos, explicit } = context
const line = state.doc.lineAt(pos)
// Determine trigger kind and character
let triggerKind: CompletionTriggerKind = CompletionTriggerKind.Invoked
let triggerCharacter: string | undefined
// Check if completion was triggered by a special character
const prevChar = line.text[pos - line.from - 1] || ''
const isTriggerChar = triggerCharacters?.includes(prevChar)
if (!explicit && isTriggerChar) {
triggerKind = CompletionTriggerKind.TriggerCharacter
triggerCharacter = prevChar
}
// For manual invocation, only show completions when typing
// Use the provided pattern or default to words, dots, commas, or slashes
if (
triggerKind === CompletionTriggerKind.Invoked &&
!context.matchBefore(matchBeforePattern || /(\w+|\w+\.|\/|,)$/)
) {
return null
}
return { triggerKind, triggerCharacter }
}

View File

@ -0,0 +1,40 @@
import type { Extension } from '@codemirror/state'
import { Prec } from '@codemirror/state'
import type { ViewPlugin } from '@codemirror/view'
import { keymap } from '@codemirror/view'
import type { LanguageServerPlugin } from './lsp'
import { offsetToPos, showErrorMessage } from './util'
export default function lspGoToDefinitionExt(
plugin: ViewPlugin<LanguageServerPlugin>
): Extension {
return [
Prec.highest(
keymap.of([
{
key: 'F12',
run: (view) => {
if (!plugin) {
return false
}
const value = view.plugin(plugin)
if (!value) return false
const pos = view.state.selection.main.head
value
.requestDefinition(view, offsetToPos(view.state.doc, pos))
.catch((error) =>
showErrorMessage(
view,
`Go to definition failed: ${error instanceof Error ? error.message : 'Unknown error'}`
)
)
return true
},
},
])
),
]
}

View File

@ -4,6 +4,7 @@ import type {
CompletionResult,
} from '@codemirror/autocomplete'
import { completeFromList, snippetCompletion } from '@codemirror/autocomplete'
import type { Action, Diagnostic } from '@codemirror/lint'
import { linter } from '@codemirror/lint'
import type { Extension, StateEffect } from '@codemirror/state'
import { Facet, Transaction } from '@codemirror/state'
@ -24,14 +25,29 @@ import { DiagnosticSeverity } from 'vscode-languageserver-protocol'
import { URI } from 'vscode-uri'
import type { LanguageServerClient } from '../client'
import { lspFormatCodeEvent, lspSemanticTokensEvent } from './annotation'
import {
lspFormatCodeEvent,
lspSemanticTokensEvent,
lspRenameEvent,
lspCodeActionEvent,
} from './annotation'
import lspAutocompleteExt, { CompletionItemKindMap } from './autocomplete'
import lspFormatExt from './format'
import lspHoverExt from './hover'
import lspIndentExt from './indent'
import type { SemanticToken } from './semantic-tokens'
import lspSemanticTokensExt, { addToken } from './semantic-tokens'
import { formatMarkdownContents, posToOffset } from './util'
import {
formatContents,
offsetToPos,
posToOffset,
posToOffsetOrZero,
showErrorMessage,
} from './util'
import { isArray } from '../lib/utils'
import lspGoToDefinitionExt from './go-to-definition'
import lspRenameExt from './rename'
import lspSignatureHelpExt from './signature-help'
const useLast = (values: readonly any[]) => values.reduce((_, v) => v, '')
export const docPathFacet = Facet.define<string, string>({
@ -43,6 +59,25 @@ export const workspaceFolders = Facet.define<
LSP.WorkspaceFolder[]
>({ combine: useLast })
const severityMap: Record<DiagnosticSeverity, Diagnostic['severity']> = {
[DiagnosticSeverity.Error]: 'error',
[DiagnosticSeverity.Warning]: 'warning',
[DiagnosticSeverity.Information]: 'info',
[DiagnosticSeverity.Hint]: 'info',
}
/**
* Result of a definition lookup operation
*/
export interface DefinitionResult {
/** URI of the target document containing the definition */
uri: string
/** Range in the document where the definition is located */
range: LSP.Range
/** Whether the definition is in a different file than the current document */
isExternalDocument: boolean
}
export interface LanguageServerOptions {
// We assume this is the main project directory, we are currently working in.
workspaceFolders: LSP.WorkspaceFolder[]
@ -58,6 +93,9 @@ export interface LanguageServerOptions {
doSemanticTokens?: boolean
doFoldingRanges?: boolean
/** Callback triggered when a go-to-definition action is performed */
onGoToDefinition?: (result: DefinitionResult) => void
}
export class LanguageServerPlugin implements PluginValue {
@ -82,6 +120,8 @@ export class LanguageServerPlugin implements PluginValue {
// document.
private sendScheduled: number | null = null
private onGoToDefinition: ((result: DefinitionResult) => void) | undefined
constructor(
options: LanguageServerOptions,
private view: EditorView
@ -104,6 +144,8 @@ export class LanguageServerPlugin implements PluginValue {
this.processLspNotification = options.processLspNotification
this.onGoToDefinition = options.onGoToDefinition
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.initialize({
documentText: this.getDocText(),
@ -185,8 +227,8 @@ export class LanguageServerPlugin implements PluginValue {
dom.classList.add('documentation')
dom.classList.add('hover-tooltip')
dom.style.zIndex = '99999999'
if (this.allowHTMLContent) dom.innerHTML = formatMarkdownContents(contents)
else dom.textContent = formatMarkdownContents(contents)
if (this.allowHTMLContent) dom.innerHTML = formatContents(contents)
else dom.textContent = formatContents(contents)
return { pos, end, create: (view) => ({ dom }), above: true }
}
@ -317,7 +359,7 @@ export class LanguageServerPlugin implements PluginValue {
triggerCharacter,
}: {
triggerKind: CompletionTriggerKind
triggerCharacter: string | undefined
triggerCharacter?: string
}
): Promise<CompletionResult | null> {
if (
@ -379,8 +421,7 @@ export class LanguageServerPlugin implements PluginValue {
const deprecatedHtml = deprecated
? '<p><strong>Deprecated</strong></p>'
: ''
const htmlString =
deprecatedHtml + formatMarkdownContents(documentation)
const htmlString = deprecatedHtml + formatContents(documentation)
const htmlNode = document.createElement('div')
htmlNode.style.display = 'contents'
htmlNode.innerHTML = htmlString
@ -406,6 +447,660 @@ export class LanguageServerPlugin implements PluginValue {
return completeFromList(options)(context)
}
async requestDefinition(
view: EditorView,
{ line, character }: { line: number; character: number }
) {
if (
!(
this.client.ready &&
this.client.getServerCapabilities().definitionProvider
)
) {
return
}
const result = await this.client.textDocumentDefinition({
textDocument: { uri: this.getDocUri() },
position: { line, character },
})
if (!result) return
const locations = isArray(result) ? result : [result]
if (locations.length === 0) return
// For now just handle the first location
const location = locations[0]
if (!location) return
const uri = 'uri' in location ? location.uri : location.targetUri
const range = 'range' in location ? location.range : location.targetRange
// Check if the definition is in a different document
const isExternalDocument = uri !== this.getDocUri()
// Create the definition result
const definitionResult: DefinitionResult = {
uri,
range,
isExternalDocument,
}
// If it's the same document, update the selection
if (!isExternalDocument) {
view.dispatch(
view.state.update({
selection: {
anchor: posToOffsetOrZero(view.state.doc, range.start),
head: posToOffset(view.state.doc, range.end),
},
})
)
}
if (this.onGoToDefinition) {
this.onGoToDefinition(definitionResult)
}
return definitionResult
}
async requestCodeActions(
range: LSP.Range,
diagnosticCodes: string[]
): Promise<(LSP.Command | LSP.CodeAction)[] | null> {
if (
!(
this.client.ready &&
this.client.getServerCapabilities().codeActionProvider
)
) {
return null
}
return await this.client.textDocumentCodeAction({
textDocument: { uri: this.getDocUri() },
range,
context: {
diagnostics: [
{
range,
code: diagnosticCodes[0],
source: this.getLanguageId(),
message: '',
},
],
},
})
}
async requestRename(
view: EditorView,
{ line, character }: { line: number; character: number }
) {
if (
!(this.client.getServerCapabilities().renameProvider && this.client.ready)
) {
showErrorMessage(view, 'Rename not supported by language server')
return
}
try {
// First check if rename is possible at this position
const prepareResult = await this.client
.textDocumentPrepareRename({
textDocument: { uri: this.getDocUri() },
position: { line, character },
})
.catch(() => {
// In case prepareRename is not supported,
// we fallback to the default implementation
return this.prepareRenameFallback(view, {
line,
character,
})
})
if (!prepareResult || 'defaultBehavior' in prepareResult) {
showErrorMessage(view, 'Cannot rename this symbol')
return
}
// Create popup input
const popup = document.createElement('div')
popup.className = 'cm-rename-popup'
popup.style.cssText =
'position: absolute; padding: 4px; background: white; border: 1px solid #ddd; box-shadow: 0 2px 8px rgba(0,0,0,.15); z-index: 99;'
const input = document.createElement('input')
input.type = 'text'
input.style.cssText =
'width: 200px; padding: 4px; border: 1px solid #ddd;'
// Get current word as default value
const range =
'range' in prepareResult ? prepareResult.range : prepareResult
const from = posToOffset(view.state.doc, range.start)
if (from == null) {
return
}
const to = posToOffset(view.state.doc, range.end)
input.value = view.state.doc.sliceString(from, to)
popup.appendChild(input)
// Position the popup near the word
const coords = view.coordsAtPos(from)
if (!coords) return
popup.style.left = `${coords.left}px`
popup.style.top = `${coords.bottom + 5}px`
// Handle input
const handleRename = async () => {
const newName = input.value.trim()
if (!newName) {
showErrorMessage(view, 'New name cannot be empty')
popup.remove()
return
}
if (newName === input.defaultValue) {
popup.remove()
return
}
try {
const edit = await this.client.textDocumentRename({
textDocument: { uri: this.getDocUri() },
position: { line, character },
newName,
})
await this.applyRenameEdit(view, edit)
} catch (error) {
showErrorMessage(
view,
`Rename failed: ${error instanceof Error ? error.message : 'Unknown error'}`
)
} finally {
popup.remove()
}
}
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
handleRename()
} else if (e.key === 'Escape') {
popup.remove()
}
e.stopPropagation() // Prevent editor handling
})
// Handle clicks outside
const handleOutsideClick = (e: MouseEvent) => {
if (!popup.contains(e.target as Node)) {
popup.remove()
document.removeEventListener('mousedown', handleOutsideClick)
}
}
document.addEventListener('mousedown', handleOutsideClick)
// Add to DOM
document.body.appendChild(popup)
input.focus()
input.select()
} catch (error) {
showErrorMessage(
view,
`Rename failed: ${error instanceof Error ? error.message : 'Unknown error'}`
)
}
}
/**
* Request signature help from the language server
* @param view The editor view
* @param position The cursor position
* @returns A tooltip with the signature help information or null if not available
*/
public async requestSignatureHelp(
view: EditorView,
{
line,
character,
}: {
line: number
character: number
},
triggerCharacter: string | undefined = undefined
): Promise<Tooltip | null> {
// Check if signature help is enabled
if (
!(
this.client.ready &&
this.client.getServerCapabilities().signatureHelpProvider
)
) {
return null
}
try {
// Request signature help
const result = await this.client.textDocumentSignatureHelp({
textDocument: { uri: this.getDocUri() },
position: { line, character },
context: {
isRetrigger: false,
triggerKind: 1, // Invoked
triggerCharacter,
},
})
if (!result?.signatures || result.signatures.length === 0) {
return null
}
// Create the tooltip container
const dom = this.createTooltipContainer()
// Get active signature
const activeSignatureIndex = result.activeSignature ?? 0
const activeSignature =
result.signatures[activeSignatureIndex] || result.signatures[0]
if (!activeSignature) {
return null
}
const activeParameterIndex =
result.activeParameter || activeSignature.activeParameter
// Create and add signature display element
const signatureElement = this.createSignatureElement(
activeSignature,
activeParameterIndex
)
dom.appendChild(signatureElement)
// Add documentation if available
if (activeSignature.documentation) {
dom.appendChild(
this.createDocumentationElement(activeSignature.documentation)
)
}
if (activeParameterIndex) {
// Add parameter documentation if available
const activeParam = activeSignature.parameters?.[activeParameterIndex]
if (activeParam?.documentation) {
dom.appendChild(this.createParameterDocElement(activeParam))
}
} else {
// Append docs for all the parameters.
activeSignature.parameters?.forEach((param) => {
if (param.documentation) {
dom.appendChild(this.createParameterDocElement(param))
}
})
}
// Position tooltip at cursor
let pos = posToOffset(view.state.doc, { line, character })
if (pos === null || pos === undefined) return null
return {
pos,
end: pos,
create: (_view) => ({ dom }),
above: true,
}
} catch (error) {
console.error('Signature help error:', error)
return null
}
}
/**
* Shows a signature help tooltip at the specified position
*/
public async showSignatureHelpTooltip(
view: EditorView,
pos: number,
triggerCharacter?: string
) {
const tooltip = await this.requestSignatureHelp(
view,
offsetToPos(view.state.doc, pos),
triggerCharacter
)
if (tooltip) {
// Create and show the tooltip manually
const { pos: tooltipPos, create } = tooltip
const tooltipView = create(view)
const tooltipElement = document.createElement('div')
tooltipElement.className =
'documentation hover-tooltip cm-tooltip cm-signature-tooltip'
tooltipElement.style.position = 'absolute'
tooltipElement.style.zIndex = '99999999'
tooltipElement.appendChild(tooltipView.dom)
// Position the tooltip
const coords = view.coordsAtPos(tooltipPos)
if (coords) {
tooltipElement.style.left = `${coords.left}px`
tooltipElement.style.top = `${coords.bottom + 5}px`
// Add to DOM
document.body.appendChild(tooltipElement)
// Remove after a delay or on editor changes
setTimeout(() => {
removeTooltip() // Use the function that also cleans up event listeners
}, 10000) // Show for 10 seconds
// Also remove on any user input
const removeTooltip = () => {
tooltipElement.remove()
view.dom.removeEventListener('keydown', removeTooltip)
view.dom.removeEventListener('mousedown', removeTooltip)
}
view.dom.addEventListener('keydown', removeTooltip)
view.dom.addEventListener('mousedown', removeTooltip)
}
}
}
/**
* Creates the main tooltip container for signature help
*/
private createTooltipContainer(): HTMLElement {
const dom = document.createElement('div')
dom.classList.add('cm-signature-help')
return dom
}
/**
* Creates the signature element with parameter highlighting
*/
private createSignatureElement(
signature: LSP.SignatureInformation,
activeParameterIndex?: number
): HTMLElement {
const signatureElement = document.createElement('div')
signatureElement.classList.add('cm-signature')
signatureElement.style.cssText =
'font-family: monospace; margin-bottom: 4px;'
if (!signature.label || typeof signature.label !== 'string') {
signatureElement.textContent = 'Signature information unavailable'
return signatureElement
}
const signatureText = signature.label
const parameters = signature.parameters || []
// If there are no parameters or no active parameter, just show the signature text
if (
parameters.length === 0 ||
!activeParameterIndex ||
!parameters[activeParameterIndex]
) {
signatureElement.textContent = signatureText
return signatureElement
}
// Handle parameter highlighting based on the parameter label type
const paramLabel = parameters[activeParameterIndex].label
if (typeof paramLabel === 'string') {
// Simple string replacement
if (this.allowHTMLContent) {
signatureElement.innerHTML = signatureText.replace(
paramLabel,
`<strong class="cm-signature-active-param">${paramLabel}</strong>`
)
} else {
signatureElement.textContent = signatureText.replace(
paramLabel,
`«${paramLabel}»`
)
}
} else if (isArray(paramLabel) && paramLabel.length === 2) {
// Handle array format [startIndex, endIndex]
this.applyRangeHighlighting(
signatureElement,
signatureText,
paramLabel[0],
paramLabel[1]
)
} else {
signatureElement.textContent = signatureText
}
return signatureElement
}
/**
* Applies parameter highlighting using a range approach
*/
private applyRangeHighlighting(
element: HTMLElement,
text: string,
startIndex: number,
endIndex: number
): void {
// Clear any existing content
element.textContent = ''
// Split the text into three parts: before, parameter, after
const beforeParam = text.substring(0, startIndex)
const param = text.substring(startIndex, endIndex)
const afterParam = text.substring(endIndex)
// Add the parts to the element
element.appendChild(document.createTextNode(beforeParam))
const paramSpan = document.createElement('span')
paramSpan.classList.add('cm-signature-active-param')
paramSpan.style.cssText = 'font-weight: bold; text-decoration: underline;'
paramSpan.textContent = param
element.appendChild(paramSpan)
element.appendChild(document.createTextNode(afterParam))
}
/**
* Creates the documentation element for signatures
*/
private createDocumentationElement(
documentation: string | LSP.MarkupContent
): HTMLElement {
const docsElement = document.createElement('div')
docsElement.classList.add('cm-signature-docs')
docsElement.style.cssText = 'margin-top: 4px; color: #666;'
const formattedContent = formatContents(documentation)
if (this.allowHTMLContent) {
docsElement.innerHTML = formattedContent
} else {
docsElement.textContent = formattedContent
}
return docsElement
}
/**
* Creates the parameter documentation element
*/
private createParameterDocElement(
parameter: LSP.ParameterInformation
): HTMLElement {
const paramDocsElement = document.createElement('div')
paramDocsElement.classList.add('cm-parameter-docs')
paramDocsElement.style.cssText =
'margin-top: 4px; font-style: italic; border-top: 1px solid #eee; padding-top: 4px;'
const formattedContent =
`<strong>${parameter.label}:</strong> ` +
formatContents(parameter.documentation)
if (this.allowHTMLContent) {
paramDocsElement.innerHTML = formattedContent
} else {
paramDocsElement.textContent = formattedContent
}
return paramDocsElement
}
/**
* Fallback implementation of prepareRename.
* We try to find the word at the cursor position and return the range of the word.
*/
private prepareRenameFallback(
view: EditorView,
{ line, character }: { line: number; character: number }
): LSP.PrepareRenameResult | null {
const doc = view.state.doc
const lineText = doc.line(line + 1).text
const wordRegex = /\w+/g
let match: RegExpExecArray | null
let start = character
let end = character
// Find all word matches in the line
// biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
while ((match = wordRegex.exec(lineText)) !== null) {
const matchStart = match.index
const matchEnd = match.index + match[0].length
// Check if cursor position is within or at the boundaries of this word
if (character >= matchStart && character <= matchEnd) {
start = matchStart
end = matchEnd
break
}
}
if (start === character && end === character) {
return null // No word found at cursor position
}
return {
range: {
start: {
line,
character: start,
},
end: {
line,
character: end,
},
},
placeholder: lineText.slice(start, end),
}
}
/**
* Apply workspace edit from rename operation
* @param view The editor view
* @param edit The workspace edit to apply
* @returns True if changes were applied successfully
*/
protected async applyRenameEdit(
view: EditorView,
edit: LSP.WorkspaceEdit | null
): Promise<boolean> {
if (!edit) {
showErrorMessage(view, 'No edit returned from language server')
return false
}
const changesMap = edit.changes ?? {}
const documentChanges = edit.documentChanges ?? []
if (Object.keys(changesMap).length === 0 && documentChanges.length === 0) {
showErrorMessage(view, 'No changes to apply')
return false
}
// Handle documentChanges (preferred) if available
if (documentChanges.length > 0) {
for (const docChange of documentChanges) {
if ('textDocument' in docChange) {
// This is a TextDocumentEdit
const uri = docChange.textDocument.uri
if (uri !== this.getDocUri()) {
showErrorMessage(view, 'Multi-file rename not supported yet')
continue
}
// Sort edits in reverse order to avoid position shifts
const sortedEdits = docChange.edits.sort((a, b) => {
const posA = posToOffset(view.state.doc, a.range.start)
const posB = posToOffset(view.state.doc, b.range.start)
return (posB ?? 0) - (posA ?? 0)
})
// Create a single transaction with all changes
const changes = sortedEdits.map((edit) => ({
from: posToOffset(view.state.doc, edit.range.start) ?? 0,
to: posToOffset(view.state.doc, edit.range.end) ?? 0,
insert: edit.newText,
annotations: [lspRenameEvent],
}))
view.dispatch(view.state.update({ changes }))
return true
}
// This is a CreateFile, RenameFile, or DeleteFile operation
showErrorMessage(
view,
'File creation, deletion, or renaming operations not supported yet'
)
return false
}
}
// Fall back to changes if documentChanges is not available
else if (Object.keys(changesMap).length > 0) {
// Apply all changes
for (const [uri, changes] of Object.entries(changesMap)) {
if (uri !== this.getDocUri()) {
showErrorMessage(view, 'Multi-file rename not supported yet')
continue
}
// Sort changes in reverse order to avoid position shifts
const sortedChanges = changes.sort((a, b) => {
const posA = posToOffset(view.state.doc, a.range.start)
const posB = posToOffset(view.state.doc, b.range.start)
return (posB ?? 0) - (posA ?? 0)
})
// Create a single transaction with all changes
const changeSpecs = sortedChanges.map((change) => ({
from: posToOffset(view.state.doc, change.range.start) ?? 0,
to: posToOffset(view.state.doc, change.range.end) ?? 0,
insert: change.newText,
}))
view.dispatch(view.state.update({ changes: changeSpecs }))
return true
}
}
return false
}
parseSemanticTokens(view: EditorView, data: number[]) {
// decode the lsp semantic token types
const tokens = []
@ -509,7 +1204,7 @@ export class LanguageServerPlugin implements PluginValue {
params
)
// this is sometimes slower than our actual typing.
this.processDiagnostics(params)
await this.processDiagnostics(params)
break
case 'window/logMessage':
console.log(
@ -534,25 +1229,74 @@ export class LanguageServerPlugin implements PluginValue {
this.processLspNotification?.(this, notification)
}
processDiagnostics(params: PublishDiagnosticsParams) {
async processDiagnostics(params: PublishDiagnosticsParams) {
if (params.uri !== this.getDocUri()) return
// Commented to avoid the lint. See TODO below.
// const diagnostics =
params.diagnostics
.map(({ range, message, severity }) => ({
from: posToOffset(this.view.state.doc, range.start)!,
to: posToOffset(this.view.state.doc, range.end)!,
severity: (
{
[DiagnosticSeverity.Error]: 'error',
[DiagnosticSeverity.Warning]: 'warning',
[DiagnosticSeverity.Information]: 'info',
[DiagnosticSeverity.Hint]: 'info',
} as const
)[severity!],
const rawDiagnostics = params.diagnostics.map(
async ({ range, message, severity, code }) => {
const actions = await this.requestCodeActions(range, [code as string])
const codemirrorActions = actions?.map(
(action): Action => ({
name:
'command' in action && typeof action.command === 'object'
? action.command?.title || action.title
: action.title,
// eslint-disable-next-line @typescript-eslint/no-misused-promises
apply: async () => {
if ('edit' in action && action.edit?.changes) {
const changes = action.edit.changes[this.getDocUri()]
if (!changes) {
return
}
// Apply workspace edit
for (const change of changes) {
this.view.dispatch(
this.view.state.update({
changes: {
from: posToOffsetOrZero(
this.view.state.doc,
change.range.start
),
to: posToOffset(this.view.state.doc, change.range.end),
insert: change.newText,
},
annotations: [lspCodeActionEvent],
})
)
}
}
if ('command' in action && action.command) {
// TODO: Implement command execution
// Execute command if present
console.warn(
'[codemirror-lsp-client/processDiagnostics] executing command:',
action.command
)
}
},
})
)
const diagnostic: Diagnostic = {
from: posToOffsetOrZero(this.view.state.doc, range.start),
to: posToOffsetOrZero(this.view.state.doc, range.end),
severity: severityMap[severity ?? DiagnosticSeverity.Error],
source: this.getLanguageId(),
actions: codemirrorActions,
message,
}))
}
return diagnostic
}
)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const diagnostics = (await Promise.all(rawDiagnostics))
.filter(
({ from, to }) =>
from !== null && to !== null && from !== undefined && to !== undefined
@ -581,12 +1325,15 @@ export class LanguageServerPluginSpec
{
provide(plugin: ViewPlugin<LanguageServerPlugin>): Extension {
return [
linter(null),
lspAutocompleteExt(plugin),
lspFormatExt(plugin),
lspGoToDefinitionExt(plugin),
lspHoverExt(plugin),
lspIndentExt(),
lspRenameExt(plugin),
lspSemanticTokensExt(),
linter(null),
lspSignatureHelpExt(plugin),
]
}
}

View File

@ -0,0 +1,32 @@
import type { Extension } from '@codemirror/state'
import { Prec } from '@codemirror/state'
import type { ViewPlugin } from '@codemirror/view'
import { keymap } from '@codemirror/view'
import type { LanguageServerPlugin } from './lsp'
import { offsetToPos } from './util'
export default function lspRenameExt(
plugin: ViewPlugin<LanguageServerPlugin>
): Extension {
return [
Prec.highest(
keymap.of([
{
key: 'F2',
run: (view) => {
if (!plugin) return false
const value = view.plugin(plugin)
if (!value) return false
const pos = view.state.selection.main.head
// eslint-disable-next-line @typescript-eslint/no-floating-promises
value.requestRename(view, offsetToPos(view.state.doc, pos))
return true
},
},
])
),
]
}

View File

@ -0,0 +1,90 @@
import type { Extension } from '@codemirror/state'
import { Prec } from '@codemirror/state'
import type { ViewPlugin } from '@codemirror/view'
import { EditorView } from '@codemirror/view'
import { keymap } from '@codemirror/view'
import type { LanguageServerPlugin } from './lsp'
export default function lspSignatureHelpExt(
plugin: ViewPlugin<LanguageServerPlugin>
): Extension {
return [
Prec.highest(
keymap.of([
{
key: 'Mod-Shift-Space',
run: (view) => {
if (!plugin) {
return false
}
const value = view.plugin(plugin)
if (!value) return false
const pos = view.state.selection.main.head
// eslint-disable-next-line @typescript-eslint/no-floating-promises
value.showSignatureHelpTooltip(view, pos)
return true
},
},
])
),
// eslint-disable-next-line @typescript-eslint/no-misused-promises
EditorView.updateListener.of(async (update) => {
if (!(plugin && update.docChanged)) return
// Make sure this is a valid user typing event.
let isRelevant = false
for (const tr of update.transactions) {
if (tr.isUserEvent('input')) {
isRelevant = true
}
}
if (!isRelevant) {
// We only want signature help on user events.
return
}
const value = update.view.plugin(plugin)
if (!value) return false
// Early exit if signature help capability is not supported
if (!value.client.getServerCapabilities().signatureHelpProvider) return
const triggerChars = value.client.getServerCapabilities()
.signatureHelpProvider?.triggerCharacters || ['(', ',']
let triggerCharacter: string | undefined
// Check if changes include trigger characters
const changes = update.changes
let shouldTrigger = false
let triggerPos = -1
changes.iterChanges((_fromA, _toA, _fromB, toB, inserted) => {
if (shouldTrigger) return // Skip if already found a trigger
const text = inserted.toString()
if (!text) return
for (const char of triggerChars) {
if (text.includes(char)) {
shouldTrigger = true
triggerPos = toB
triggerCharacter = char
break
}
}
})
if (shouldTrigger && triggerPos >= 0) {
await value.showSignatureHelpTooltip(
update.view,
triggerPos,
triggerCharacter
)
}
}),
]
}

View File

@ -2,6 +2,7 @@ import type { Text } from '@codemirror/state'
import type { MarkedOptions } from '@ts-stack/markdown'
import { Marked } from '@ts-stack/markdown'
import type * as LSP from 'vscode-languageserver-protocol'
import type { EditorView } from '@codemirror/view'
import { isArray } from '../lib/utils'
@ -36,6 +37,13 @@ export function posToOffset(
return offset
}
export function posToOffsetOrZero(
doc: Text,
pos: { line: number; character: number }
): number {
return posToOffset(doc, pos) || 0
}
export function offsetToPos(doc: Text, offset: number) {
const line = doc.lineAt(offset)
return {
@ -48,14 +56,84 @@ const markedOptions: MarkedOptions = {
gfm: true,
}
export function formatMarkdownContents(
export function isLSPTextEdit(
textEdit?: LSP.TextEdit | LSP.InsertReplaceEdit
): textEdit is LSP.TextEdit {
return (textEdit as LSP.TextEdit)?.range !== undefined
}
export function isLSPMarkupContent(
contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[]
): contents is LSP.MarkupContent {
return (contents as LSP.MarkupContent).kind !== undefined
}
export function formatContents(
contents:
| LSP.MarkupContent
| LSP.MarkedString
| LSP.MarkedString[]
| undefined
): string {
if (!contents) {
return ''
}
if (isLSPMarkupContent(contents)) {
let value = contents.value
if (contents.kind === 'markdown') {
value = Marked.parse(value, markedOptions)
}
return value
}
if (isArray(contents)) {
return contents.map((c) => formatMarkdownContents(c) + '\n\n').join('')
} else if (typeof contents === 'string') {
return Marked.parse(contents, markedOptions)
} else {
return Marked.parse(contents.value, markedOptions)
return contents
.map((c) => formatContents(c))
.filter(Boolean)
.join('\n\n')
}
if (typeof contents === 'string') {
return contents
}
if (
typeof contents === 'object' &&
'language' in contents &&
'value' in contents
) {
return contents.value
}
return ''
}
export function showErrorMessage(view: EditorView, message: string) {
const tooltip = document.createElement('div')
tooltip.className = 'cm-error-message'
tooltip.style.cssText = `
position: absolute;
padding: 8px;
background: #fee;
border: 1px solid #fcc;
border-radius: 4px;
color: #c00;
font-size: 14px;
z-index: 100;
max-width: 300px;
box-shadow: 0 2px 8px rgba(0,0,0,.15);
`
tooltip.textContent = message
// Position near the cursor
const cursor = view.coordsAtPos(view.state.selection.main.head)
if (cursor) {
tooltip.style.left = `${cursor.left}px`
tooltip.style.top = `${cursor.bottom + 5}px`
}
document.body.appendChild(tooltip)
// Remove after 3 seconds
setTimeout(() => {
tooltip.style.opacity = '0'
tooltip.style.transition = 'opacity 0.2s'
setTimeout(() => tooltip.remove(), 200)
}, 3000)
}

View File

@ -1287,6 +1287,69 @@ impl LanguageServer for Backend {
let pos = position_to_char_index(params.text_document_position_params.position, current_code);
// Get the character at the position.
let Some(ch) = current_code.chars().nth(pos) else {
return Ok(None);
};
let check_char = |ch: char| {
// If we are on a (, then get the string in front of the (
// and try to get the signature.
// We do these before the ast check because we might not have a valid ast.
if ch == '(' {
// If the current character is not a " " then get the next space after
// our position so we can split on that.
// Find the next space after the current position.
let next_space = if ch != ' ' {
if let Some(next_space) = current_code[pos..].find(' ') {
pos + next_space
} else if let Some(next_space) = current_code[pos..].find('(') {
pos + next_space
} else {
pos
}
} else {
pos
};
let p2 = std::cmp::max(pos, next_space);
let last_word = current_code[..p2].split_whitespace().last()?;
// Get the function name.
return self.stdlib_signatures.get(last_word);
} else if ch == ',' {
// If we have a comma, then get the string in front of
// the closest ( and try to get the signature.
// Find the last ( before the comma.
let last_paren = current_code[..pos].rfind('(')?;
// Get the string in front of the (.
let last_word = current_code[..last_paren].split_whitespace().last()?;
// Get the function name.
return self.stdlib_signatures.get(last_word);
}
None
};
if let Some(signature) = check_char(ch) {
return Ok(Some(signature.clone()));
}
// Check if we have context.
if let Some(context) = params.context {
if let Some(character) = context.trigger_character {
for character in character.chars() {
// Check if we are on a ( or a ,.
if character == '(' || character == ',' {
if let Some(signature) = check_char(character) {
return Ok(Some(signature.clone()));
}
}
}
}
}
// Let's iterate over the AST and find the node that contains the cursor.
let Some(ast) = self.ast_map.get(&filename) else {
return Ok(None);
@ -1419,7 +1482,7 @@ impl LanguageServer for Backend {
ast.rename_symbol(&params.new_name, pos);
// Now recast it.
let recast = ast.recast(&Default::default(), 0);
let source_range = SourceRange::new(0, current_code.len() - 1, module_id);
let source_range = SourceRange::new(0, current_code.len(), module_id);
let range = source_range.to_lsp_range(current_code);
Ok(Some(WorkspaceEdit {
changes: Some(HashMap::from([(
@ -1590,7 +1653,7 @@ fn position_to_char_index(position: Position, code: &str) -> usize {
}
}
char_position
std::cmp::min(char_position, code.len() - 1)
}
async fn with_cached_var<T>(name: &str, f: impl Fn(&KclValue) -> T) -> Option<T> {

View File

@ -1119,6 +1119,348 @@ async fn test_kcl_lsp_signature_help() {
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_kcl_lsp_signature_help_on_parens_trigger() {
let server = kcl_lsp_server(false).await.unwrap();
// Send open file.
// We do this to trigger a valid ast.
server
.did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams {
text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
version: 1,
},
content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent {
range: None,
range_length: None,
text: "myVarName = 100
a1 = startSketchOn(offsetPlane(XY, offset = 10))
|> startProfile(at = [0, 0])
|> line(end = [myVarName, 0])
|> yLine(length = -100.0)
|> xLine(length = -100.0)
|> yLine(length = 100.0)
|> close()"
.to_string(),
}],
})
.await;
// Send open file.
server
.did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams {
text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
version: 1,
},
content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent {
range: None,
range_length: None,
text: "myVarName = 100
a1 = startSketchOn(offsetPlane(XY, offset = 10))
|> startProfile(at = [0, 0])
|> line(end = [myVarName, 0])
|> yLine(length = -100.0)
|> xLine(length = -100.0)
|> yLine(length = 100.0)
|> close()
|> extrude("
.to_string(),
}],
})
.await;
// Send signature help request.
let signature_help = server
.signature_help(tower_lsp::lsp_types::SignatureHelpParams {
text_document_position_params: tower_lsp::lsp_types::TextDocumentPositionParams {
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
position: tower_lsp::lsp_types::Position { line: 9, character: 14 },
},
context: None,
work_done_progress_params: Default::default(),
})
.await
.unwrap();
// Check the signature help.
if let Some(signature_help) = signature_help {
assert_eq!(
signature_help.signatures.len(),
1,
"Expected one signature, got {:?}",
signature_help.signatures
);
assert_eq!(signature_help.signatures[0].label, "extrude");
} else {
panic!("Expected signature help");
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_kcl_lsp_signature_help_on_parens_trigger_on_before() {
let server = kcl_lsp_server(false).await.unwrap();
// Send open file.
// We do this to trigger a valid ast.
server
.did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams {
text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
version: 1,
},
content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent {
range: None,
range_length: None,
text: "myVarName = 100
a1 = startSketchOn(offsetPlane(XY, offset = 10))
|> startProfile(at = [0, 0])
|> line(end = [myVarName, 0])
|> yLine(length = -100.0)
|> xLine(length = -100.0)
|> yLine(length = 100.0)
|> close()"
.to_string(),
}],
})
.await;
// Send open file.
server
.did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams {
text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
version: 1,
},
content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent {
range: None,
range_length: None,
text: "myVarName = 100
a1 = startSketchOn(offsetPlane(XY, offset = 10))
|> startProfile(at = [0, 0])
|> line(end = [myVarName, 0])
|> yLine(length = -100.0)
|> xLine(length = -100.0)
|> yLine(length = 100.0)
|> close()
|> extrude("
.to_string(),
}],
})
.await;
// Send signature help request.
let signature_help = server
.signature_help(tower_lsp::lsp_types::SignatureHelpParams {
text_document_position_params: tower_lsp::lsp_types::TextDocumentPositionParams {
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
position: tower_lsp::lsp_types::Position { line: 9, character: 10 },
},
context: Some(tower_lsp::lsp_types::SignatureHelpContext {
trigger_kind: tower_lsp::lsp_types::SignatureHelpTriggerKind::INVOKED,
trigger_character: Some("(".to_string()),
is_retrigger: false,
active_signature_help: None,
}),
work_done_progress_params: Default::default(),
})
.await
.unwrap();
// Check the signature help.
if let Some(signature_help) = signature_help {
assert_eq!(
signature_help.signatures.len(),
1,
"Expected one signature, got {:?}",
signature_help.signatures
);
assert_eq!(signature_help.signatures[0].label, "extrude");
} else {
panic!("Expected signature help");
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_kcl_lsp_signature_help_on_comma_trigger() {
let server = kcl_lsp_server(false).await.unwrap();
// Send open file.
// We do this to trigger a valid ast.
server
.did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams {
text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
version: 1,
},
content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent {
range: None,
range_length: None,
text: "myVarName = 100
a1 = startSketchOn(offsetPlane(XY, offset = 10))
|> startProfile(at = [0, 0])
|> line(end = [myVarName, 0])
|> yLine(length = -100.0)
|> xLine(length = -100.0)
|> yLine(length = 100.0)
|> close()"
.to_string(),
}],
})
.await;
// Send update file.
server
.did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams {
text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
version: 1,
},
content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent {
range: None,
range_length: None,
text: "myVarName = 100
a1 = startSketchOn(offsetPlane(XY, offset = 10))
|> startProfile(at = [0, 0])
|> line(end = [myVarName, 0])
|> yLine(length = -100.0)
|> xLine(length = -100.0)
|> yLine(length = 100.0)
|> close()
|> extrude(length = 10,"
.to_string(),
}],
})
.await;
// Send signature help request.
let signature_help = server
.signature_help(tower_lsp::lsp_types::SignatureHelpParams {
text_document_position_params: tower_lsp::lsp_types::TextDocumentPositionParams {
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
position: tower_lsp::lsp_types::Position { line: 9, character: 25 },
},
context: None,
work_done_progress_params: Default::default(),
})
.await
.unwrap();
// Check the signature help.
if let Some(signature_help) = signature_help {
assert_eq!(
signature_help.signatures.len(),
1,
"Expected one signature, got {:?}",
signature_help.signatures
);
assert_eq!(signature_help.signatures[0].label, "extrude");
} else {
panic!("Expected signature help");
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_kcl_lsp_signature_help_on_comma_trigger_on_before() {
let server = kcl_lsp_server(false).await.unwrap();
// Send open file.
// We do this to trigger a valid ast.
server
.did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams {
text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
version: 1,
},
content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent {
range: None,
range_length: None,
text: "myVarName = 100
a1 = startSketchOn(offsetPlane(XY, offset = 10))
|> startProfile(at = [0, 0])
|> line(end = [myVarName, 0])
|> yLine(length = -100.0)
|> xLine(length = -100.0)
|> yLine(length = 100.0)
|> close()"
.to_string(),
}],
})
.await;
// Send update file.
server
.did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams {
text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
version: 1,
},
content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent {
range: None,
range_length: None,
text: "myVarName = 100
a1 = startSketchOn(offsetPlane(XY, offset = 10))
|> startProfile(at = [0, 0])
|> line(end = [myVarName, 0])
|> yLine(length = -100.0)
|> xLine(length = -100.0)
|> yLine(length = 100.0)
|> close()
|> extrude(length = 10,"
.to_string(),
}],
})
.await;
// Send signature help request.
let signature_help = server
.signature_help(tower_lsp::lsp_types::SignatureHelpParams {
text_document_position_params: tower_lsp::lsp_types::TextDocumentPositionParams {
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
position: tower_lsp::lsp_types::Position { line: 9, character: 22 },
},
context: Some(tower_lsp::lsp_types::SignatureHelpContext {
trigger_kind: tower_lsp::lsp_types::SignatureHelpTriggerKind::INVOKED,
trigger_character: Some(",".to_string()),
is_retrigger: false,
active_signature_help: None,
}),
work_done_progress_params: Default::default(),
})
.await
.unwrap();
// Check the signature help.
if let Some(signature_help) = signature_help {
assert_eq!(
signature_help.signatures.len(),
1,
"Expected one signature, got {:?}",
signature_help.signatures
);
assert_eq!(signature_help.signatures[0].label, "extrude");
} else {
panic!("Expected signature help");
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_kcl_lsp_semantic_tokens() {
let server = kcl_lsp_server(false).await.unwrap();
@ -1808,13 +2150,91 @@ async fn test_kcl_lsp_rename() {
vec![tower_lsp::lsp_types::TextEdit {
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position { line: 0, character: 0 },
end: tower_lsp::lsp_types::Position { line: 0, character: 7 }
end: tower_lsp::lsp_types::Position { line: 0, character: 8 }
},
new_text: "newName = 1\n".to_string()
}]
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_kcl_lsp_rename_no_hanging_parens() {
let server = kcl_lsp_server(false).await.unwrap();
let code = r#"myVARName = 100
a1 = startSketchOn(offsetPlane(XY, offset = 10))
|> startProfile(at = [0, 0])
|> line(end = [myVARName, 0])
|> yLine(length = -100.0)
|> xLine(length = -100.0)
|> yLine(length = 100.0)
|> close()
|> extrude(length = 3.14)"#;
// Send open file.
server
.did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams {
text_document: tower_lsp::lsp_types::TextDocumentItem {
uri: "file:///test.kcl".try_into().unwrap(),
language_id: "kcl".to_string(),
version: 1,
text: code.to_string(),
},
})
.await;
// Send rename request.
let rename = server
.rename(tower_lsp::lsp_types::RenameParams {
text_document_position: tower_lsp::lsp_types::TextDocumentPositionParams {
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
position: tower_lsp::lsp_types::Position { line: 0, character: 2 },
},
new_name: "myVarName".to_string(),
work_done_progress_params: Default::default(),
})
.await
.unwrap()
.unwrap();
// Check the rename.
let changes = rename.changes.unwrap();
let last_character = 27;
// Get the last character of the last line of the original code.
assert_eq!(code.lines().last().unwrap().chars().count(), last_character);
let u: tower_lsp::lsp_types::Url = "file:///test.kcl".try_into().unwrap();
assert_eq!(
changes.get(&u).unwrap().clone(),
vec![tower_lsp::lsp_types::TextEdit {
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position { line: 0, character: 0 },
// Its important we get back the right number here so that we actually replace the whole text!!
end: tower_lsp::lsp_types::Position {
line: 9,
character: last_character as u32
}
},
new_text: "myVarName = 100
a1 = startSketchOn(offsetPlane(XY, offset = 10))
|> startProfile(at = [0, 0])
|> line(end = [myVarName, 0])
|> yLine(length = -100.0)
|> xLine(length = -100.0)
|> yLine(length = 100.0)
|> close()
|> extrude(length = 3.14)\n"
.to_string()
}]
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_kcl_lsp_diagnostic_no_errors() {
let server = kcl_lsp_server(false).await.unwrap();

View File

@ -2644,3 +2644,45 @@ mod import_mesh_clone {
super::execute(TEST_NAME, true).await
}
}
mod clone_w_fillets {
const TEST_NAME: &str = "clone_w_fillets";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[tokio::test(flavor = "multi_thread")]
async fn unparse() {
super::unparse(TEST_NAME).await
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, true).await
}
}
mod clone_w_shell {
const TEST_NAME: &str = "clone_w_shell";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[tokio::test(flavor = "multi_thread")]
async fn unparse() {
super::unparse(TEST_NAME).await
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, true).await
}
}

View File

@ -0,0 +1,551 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Artifact commands clone_w_fillets.kcl
---
[
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "edge_lines_visible",
"hidden": false
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "object_visible",
"object_id": "[uuid]",
"hidden": true
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "object_visible",
"object_id": "[uuid]",
"hidden": true
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "make_plane",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"x_axis": {
"x": 1.0,
"y": 0.0,
"z": 0.0
},
"y_axis": {
"x": 0.0,
"y": 1.0,
"z": 0.0
},
"size": 60.0,
"clobber": false,
"hide": true
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "enable_sketch_mode",
"entity_id": "[uuid]",
"ortho": false,
"animated": false,
"adjust_camera": false,
"planar_normal": {
"x": 0.0,
"y": 0.0,
"z": 1.0
}
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "move_path_pen",
"path": "[uuid]",
"to": {
"x": -10.0,
"y": -5.0,
"z": 0.0
}
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "sketch_mode_disable"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "start_path"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "extend_path",
"path": "[uuid]",
"segment": {
"type": "line",
"end": {
"x": 10.0,
"y": -5.0,
"z": 0.0
},
"relative": false
}
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "extend_path",
"path": "[uuid]",
"segment": {
"type": "line",
"end": {
"x": 10.0,
"y": 5.0,
"z": 0.0
},
"relative": false
}
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "extend_path",
"path": "[uuid]",
"segment": {
"type": "line",
"end": {
"x": -10.0,
"y": 5.0,
"z": 0.0
},
"relative": false
}
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "close_path",
"path_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "enable_sketch_mode",
"entity_id": "[uuid]",
"ortho": false,
"animated": false,
"adjust_camera": false,
"planar_normal": {
"x": 0.0,
"y": 0.0,
"z": 1.0
}
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "extrude",
"target": "[uuid]",
"distance": 1.0,
"faces": null,
"opposite": "None"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "object_bring_to_front",
"object_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "sketch_mode_disable"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_all_edge_faces",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_all_edge_faces",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_all_edge_faces",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_all_edge_faces",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_all_edge_faces",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_all_edge_faces",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_all_edge_faces",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_all_edge_faces",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_all_edge_faces",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_all_edge_faces",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_all_edge_faces",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_all_edge_faces",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_extrusion_face_info",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_next_adjacent_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_next_adjacent_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_next_adjacent_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_next_adjacent_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_opposite_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_opposite_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_opposite_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_opposite_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_fillet_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"radius": 2.0,
"tolerance": 0.0000001,
"cut_type": "fillet"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_fillet_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"radius": 2.0,
"tolerance": 0.0000001,
"cut_type": "fillet"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_fillet_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"radius": 2.0,
"tolerance": 0.0000001,
"cut_type": "fillet"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_fillet_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"radius": 2.0,
"tolerance": 0.0000001,
"cut_type": "fillet"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_next_adjacent_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_next_adjacent_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_next_adjacent_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_next_adjacent_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "entity_clone",
"entity_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "entity_get_all_child_uuids",
"entity_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "entity_get_all_child_uuids",
"entity_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "object_bring_to_front",
"object_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_extrusion_face_info",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "set_object_transform",
"object_id": "[uuid]",
"transforms": [
{
"translate": {
"property": {
"x": 0.0,
"y": 0.0,
"z": 20.0
},
"set": false,
"is_local": true
},
"rotate_rpy": null,
"rotate_angle_axis": null,
"scale": null
}
]
}
}
]

View File

@ -0,0 +1,6 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Artifact graph flowchart clone_w_fillets.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,80 @@
```mermaid
flowchart LR
subgraph path2 [Path]
2["Path<br>[100, 140, 0]"]
3["Segment<br>[146, 200, 0]"]
4["Segment<br>[206, 259, 0]"]
5["Segment<br>[265, 319, 0]"]
6["Segment<br>[325, 344, 0]"]
7[Solid2d]
end
1["Plane<br>[77, 94, 0]"]
8["Sweep Extrusion<br>[362, 410, 0]"]
9[Wall]
10[Wall]
11[Wall]
12[Wall]
13["Cap Start"]
14["Cap End"]
15["SweepEdge Opposite"]
16["SweepEdge Opposite"]
17["SweepEdge Opposite"]
18["SweepEdge Opposite"]
19["SweepEdge Adjacent"]
20["SweepEdge Adjacent"]
21["SweepEdge Adjacent"]
22["SweepEdge Adjacent"]
23["EdgeCut Fillet<br>[416, 609, 0]"]
24["EdgeCut Fillet<br>[416, 609, 0]"]
25["EdgeCut Fillet<br>[416, 609, 0]"]
26["EdgeCut Fillet<br>[416, 609, 0]"]
1 --- 2
2 --- 3
2 --- 4
2 --- 5
2 --- 6
2 --- 7
2 ---- 8
3 --- 12
3 x--> 13
3 --- 16
3 --- 22
4 --- 10
4 x--> 13
4 --- 15
4 --- 20
5 --- 9
5 x--> 13
5 --- 18
5 --- 19
6 --- 11
6 x--> 13
6 --- 17
6 --- 21
8 --- 9
8 --- 10
8 --- 11
8 --- 12
8 --- 13
8 --- 14
8 --- 15
8 --- 16
8 --- 17
8 --- 18
8 --- 19
8 --- 20
8 --- 21
8 --- 22
18 <--x 9
15 <--x 10
17 <--x 11
16 <--x 12
15 <--x 14
16 <--x 14
17 <--x 14
18 <--x 14
19 <--x 26
20 <--x 25
21 <--x 24
22 <--x 23
```

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
width = 20
length = 10
thickness = 1
filletRadius = 2
mountingPlateSketch = startSketchOn(XY)
|> startProfile(at = [-width/2, -length/2])
|> line(endAbsolute = [width/2, -length/2], tag = $edge1)
|> line(endAbsolute = [width/2, length/2], tag = $edge2)
|> line(endAbsolute = [-width/2, length/2], tag = $edge3)
|> close(tag = $edge4)
mountingPlate = extrude(mountingPlateSketch, length = thickness)
|> fillet(
radius = filletRadius,
tags = [
getNextAdjacentEdge(edge1),
getNextAdjacentEdge(edge2),
getNextAdjacentEdge(edge3),
getNextAdjacentEdge(edge4)
],
)
mountingPlate2 = clone(mountingPlate)
|> translate(z = 20)

View File

@ -0,0 +1,126 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Operations executed clone_w_fillets.kcl
---
[
{
"labeledArgs": {
"planeOrSolid": {
"value": {
"type": "Plane",
"artifact_id": "[uuid]"
},
"sourceRange": []
}
},
"name": "startSketchOn",
"sourceRange": [],
"type": "StdLibCall",
"unlabeledArg": null
},
{
"labeledArgs": {
"length": {
"value": {
"type": "Number",
"value": 1.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"sourceRange": []
}
},
"name": "extrude",
"sourceRange": [],
"type": "StdLibCall",
"unlabeledArg": {
"value": {
"type": "Sketch",
"value": {
"artifactId": "[uuid]"
}
},
"sourceRange": []
}
},
{
"type": "KclStdLibCall",
"name": "fillet",
"unlabeledArg": {
"value": {
"type": "Solid",
"value": {
"artifactId": "[uuid]"
}
},
"sourceRange": []
},
"labeledArgs": {
"radius": {
"value": {
"type": "Number",
"value": 2.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"sourceRange": []
},
"tags": {
"value": {
"type": "Array",
"value": [
{
"type": "Uuid",
"value": "[uuid]"
},
{
"type": "Uuid",
"value": "[uuid]"
},
{
"type": "Uuid",
"value": "[uuid]"
},
{
"type": "Uuid",
"value": "[uuid]"
}
]
},
"sourceRange": []
}
},
"sourceRange": []
},
{
"labeledArgs": {
"geometry": {
"value": {
"type": "Solid",
"value": {
"artifactId": "[uuid]"
}
},
"sourceRange": []
}
},
"name": "clone",
"sourceRange": [],
"type": "StdLibCall",
"unlabeledArg": null
}
]

View File

@ -0,0 +1,771 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Variables in memory after executing clone_w_fillets.kcl
---
{
"edge1": {
"type": "TagIdentifier",
"type": "TagIdentifier",
"value": "edge1"
},
"edge2": {
"type": "TagIdentifier",
"type": "TagIdentifier",
"value": "edge2"
},
"edge3": {
"type": "TagIdentifier",
"type": "TagIdentifier",
"value": "edge3"
},
"edge4": {
"type": "TagIdentifier",
"type": "TagIdentifier",
"value": "edge4"
},
"filletRadius": {
"type": "Number",
"value": 2.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"length": {
"type": "Number",
"value": 10.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"mountingPlate": {
"type": "Solid",
"value": {
"type": "Solid",
"id": "[uuid]",
"artifactId": "[uuid]",
"value": [
{
"faceId": "[uuid]",
"id": "[uuid]",
"sourceRange": [],
"tag": {
"commentStart": 193,
"end": 199,
"start": 193,
"type": "TagDeclarator",
"value": "edge1"
},
"type": "extrudePlane"
},
{
"faceId": "[uuid]",
"id": "[uuid]",
"sourceRange": [],
"tag": {
"commentStart": 252,
"end": 258,
"start": 252,
"type": "TagDeclarator",
"value": "edge2"
},
"type": "extrudePlane"
},
{
"faceId": "[uuid]",
"id": "[uuid]",
"sourceRange": [],
"tag": {
"commentStart": 312,
"end": 318,
"start": 312,
"type": "TagDeclarator",
"value": "edge3"
},
"type": "extrudePlane"
},
{
"faceId": "[uuid]",
"id": "[uuid]",
"sourceRange": [],
"tag": {
"commentStart": 337,
"end": 343,
"start": 337,
"type": "TagDeclarator",
"value": "edge4"
},
"type": "extrudePlane"
}
],
"sketch": {
"type": "Sketch",
"id": "[uuid]",
"paths": [
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
-10.0,
-5.0
],
"tag": {
"commentStart": 193,
"end": 199,
"start": 193,
"type": "TagDeclarator",
"value": "edge1"
},
"to": [
10.0,
-5.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
10.0,
-5.0
],
"tag": {
"commentStart": 252,
"end": 258,
"start": 252,
"type": "TagDeclarator",
"value": "edge2"
},
"to": [
10.0,
5.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
10.0,
5.0
],
"tag": {
"commentStart": 312,
"end": 318,
"start": 312,
"type": "TagDeclarator",
"value": "edge3"
},
"to": [
-10.0,
5.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
-10.0,
5.0
],
"tag": {
"commentStart": 337,
"end": 343,
"start": 337,
"type": "TagDeclarator",
"value": "edge4"
},
"to": [
-10.0,
-5.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
}
],
"on": {
"type": "plane",
"id": "[uuid]",
"artifactId": "[uuid]",
"value": "XY",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"xAxis": {
"x": 1.0,
"y": 0.0,
"z": 0.0,
"units": {
"type": "Unknown"
}
},
"yAxis": {
"x": 0.0,
"y": 1.0,
"z": 0.0,
"units": {
"type": "Unknown"
}
}
},
"start": {
"from": [
-10.0,
-5.0
],
"to": [
-10.0,
-5.0
],
"units": {
"type": "Mm"
},
"tag": null,
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
}
},
"tags": {
"edge1": {
"type": "TagIdentifier",
"value": "edge1"
},
"edge2": {
"type": "TagIdentifier",
"value": "edge2"
},
"edge3": {
"type": "TagIdentifier",
"value": "edge3"
},
"edge4": {
"type": "TagIdentifier",
"value": "edge4"
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
}
},
"height": 1.0,
"startCapId": "[uuid]",
"endCapId": "[uuid]",
"edgeCuts": [
{
"type": "fillet",
"id": "[uuid]",
"radius": {
"n": 2.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"edgeId": "[uuid]",
"tag": null
},
{
"type": "fillet",
"id": "[uuid]",
"radius": {
"n": 2.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"edgeId": "[uuid]",
"tag": null
},
{
"type": "fillet",
"id": "[uuid]",
"radius": {
"n": 2.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"edgeId": "[uuid]",
"tag": null
},
{
"type": "fillet",
"id": "[uuid]",
"radius": {
"n": 2.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"edgeId": "[uuid]",
"tag": null
}
],
"units": {
"type": "Mm"
},
"sectional": false
}
},
"mountingPlate2": {
"type": "Solid",
"value": {
"type": "Solid",
"id": "[uuid]",
"artifactId": "[uuid]",
"value": [],
"sketch": {
"type": "Sketch",
"id": "[uuid]",
"paths": [
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
-10.0,
-5.0
],
"tag": {
"commentStart": 193,
"end": 199,
"start": 193,
"type": "TagDeclarator",
"value": "edge1"
},
"to": [
10.0,
-5.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
10.0,
-5.0
],
"tag": {
"commentStart": 252,
"end": 258,
"start": 252,
"type": "TagDeclarator",
"value": "edge2"
},
"to": [
10.0,
5.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
10.0,
5.0
],
"tag": {
"commentStart": 312,
"end": 318,
"start": 312,
"type": "TagDeclarator",
"value": "edge3"
},
"to": [
-10.0,
5.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
-10.0,
5.0
],
"tag": {
"commentStart": 337,
"end": 343,
"start": 337,
"type": "TagDeclarator",
"value": "edge4"
},
"to": [
-10.0,
-5.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
}
],
"on": {
"type": "plane",
"id": "[uuid]",
"artifactId": "[uuid]",
"value": "XY",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"xAxis": {
"x": 1.0,
"y": 0.0,
"z": 0.0,
"units": {
"type": "Unknown"
}
},
"yAxis": {
"x": 0.0,
"y": 1.0,
"z": 0.0,
"units": {
"type": "Unknown"
}
}
},
"start": {
"from": [
-10.0,
-5.0
],
"to": [
-10.0,
-5.0
],
"units": {
"type": "Mm"
},
"tag": null,
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
}
},
"tags": {
"edge1": {
"type": "TagIdentifier",
"value": "edge1"
},
"edge2": {
"type": "TagIdentifier",
"value": "edge2"
},
"edge3": {
"type": "TagIdentifier",
"value": "edge3"
},
"edge4": {
"type": "TagIdentifier",
"value": "edge4"
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
}
},
"height": 1.0,
"startCapId": null,
"endCapId": null,
"units": {
"type": "Mm"
},
"sectional": false
}
},
"mountingPlateSketch": {
"type": "Sketch",
"value": {
"type": "Sketch",
"id": "[uuid]",
"paths": [
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
-10.0,
-5.0
],
"tag": {
"commentStart": 193,
"end": 199,
"start": 193,
"type": "TagDeclarator",
"value": "edge1"
},
"to": [
10.0,
-5.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
10.0,
-5.0
],
"tag": {
"commentStart": 252,
"end": 258,
"start": 252,
"type": "TagDeclarator",
"value": "edge2"
},
"to": [
10.0,
5.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
10.0,
5.0
],
"tag": {
"commentStart": 312,
"end": 318,
"start": 312,
"type": "TagDeclarator",
"value": "edge3"
},
"to": [
-10.0,
5.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
-10.0,
5.0
],
"tag": {
"commentStart": 337,
"end": 343,
"start": 337,
"type": "TagDeclarator",
"value": "edge4"
},
"to": [
-10.0,
-5.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
}
],
"on": {
"type": "plane",
"id": "[uuid]",
"artifactId": "[uuid]",
"value": "XY",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"xAxis": {
"x": 1.0,
"y": 0.0,
"z": 0.0,
"units": {
"type": "Unknown"
}
},
"yAxis": {
"x": 0.0,
"y": 1.0,
"z": 0.0,
"units": {
"type": "Unknown"
}
}
},
"start": {
"from": [
-10.0,
-5.0
],
"to": [
-10.0,
-5.0
],
"units": {
"type": "Mm"
},
"tag": null,
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
}
},
"tags": {
"edge1": {
"type": "TagIdentifier",
"value": "edge1"
},
"edge2": {
"type": "TagIdentifier",
"value": "edge2"
},
"edge3": {
"type": "TagIdentifier",
"value": "edge3"
},
"edge4": {
"type": "TagIdentifier",
"value": "edge4"
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
}
}
},
"thickness": {
"type": "Number",
"value": 1.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"width": {
"type": "Number",
"value": 20.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -0,0 +1,29 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Result of unparsing clone_w_fillets.kcl
---
width = 20
length = 10
thickness = 1
filletRadius = 2
mountingPlateSketch = startSketchOn(XY)
|> startProfile(at = [-width / 2, -length / 2])
|> line(endAbsolute = [width / 2, -length / 2], tag = $edge1)
|> line(endAbsolute = [width / 2, length / 2], tag = $edge2)
|> line(endAbsolute = [-width / 2, length / 2], tag = $edge3)
|> close(tag = $edge4)
mountingPlate = extrude(mountingPlateSketch, length = thickness)
|> fillet(
radius = filletRadius,
tags = [
getNextAdjacentEdge(edge1),
getNextAdjacentEdge(edge2),
getNextAdjacentEdge(edge3),
getNextAdjacentEdge(edge4)
],
)
mountingPlate2 = clone(mountingPlate)
|> translate(z = 20)

View File

@ -0,0 +1,476 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Artifact commands clone_w_shell.kcl
---
[
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "edge_lines_visible",
"hidden": false
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "object_visible",
"object_id": "[uuid]",
"hidden": true
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "object_visible",
"object_id": "[uuid]",
"hidden": true
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "make_plane",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"x_axis": {
"x": 1.0,
"y": 0.0,
"z": 0.0
},
"y_axis": {
"x": 0.0,
"y": 1.0,
"z": 0.0
},
"size": 60.0,
"clobber": false,
"hide": true
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "enable_sketch_mode",
"entity_id": "[uuid]",
"ortho": false,
"animated": false,
"adjust_camera": false,
"planar_normal": {
"x": 0.0,
"y": 0.0,
"z": 1.0
}
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "move_path_pen",
"path": "[uuid]",
"to": {
"x": -12.0,
"y": 12.0,
"z": 0.0
}
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "sketch_mode_disable"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "start_path"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "extend_path",
"path": "[uuid]",
"segment": {
"type": "line",
"end": {
"x": 24.0,
"y": 0.0,
"z": 0.0
},
"relative": true
}
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "extend_path",
"path": "[uuid]",
"segment": {
"type": "line",
"end": {
"x": 0.0,
"y": -24.0,
"z": 0.0
},
"relative": true
}
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "extend_path",
"path": "[uuid]",
"segment": {
"type": "line",
"end": {
"x": -24.0,
"y": 0.0,
"z": 0.0
},
"relative": true
}
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "close_path",
"path_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "enable_sketch_mode",
"entity_id": "[uuid]",
"ortho": false,
"animated": false,
"adjust_camera": false,
"planar_normal": {
"x": 0.0,
"y": 0.0,
"z": 1.0
}
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "extrude",
"target": "[uuid]",
"distance": 6.0,
"faces": null,
"opposite": "None"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "object_bring_to_front",
"object_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "sketch_mode_disable"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_all_edge_faces",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_all_edge_faces",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_all_edge_faces",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_all_edge_faces",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_all_edge_faces",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_all_edge_faces",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_all_edge_faces",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_all_edge_faces",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_all_edge_faces",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_all_edge_faces",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_all_edge_faces",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_all_edge_faces",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_extrusion_face_info",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_next_adjacent_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_next_adjacent_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_next_adjacent_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_next_adjacent_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_opposite_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_opposite_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_opposite_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_opposite_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_shell_face",
"object_id": "[uuid]",
"face_ids": [
"[uuid]"
],
"shell_thickness": 0.25,
"hollow": false
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "entity_clone",
"entity_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "entity_get_all_child_uuids",
"entity_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "entity_get_all_child_uuids",
"entity_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "object_bring_to_front",
"object_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_extrusion_face_info",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "set_object_transform",
"object_id": "[uuid]",
"transforms": [
{
"translate": {
"property": {
"x": 50.0,
"y": 0.0,
"z": 0.0
},
"set": false,
"is_local": true
},
"rotate_rpy": null,
"rotate_angle_axis": null,
"scale": null
}
]
}
}
]

View File

@ -0,0 +1,6 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Artifact graph flowchart clone_w_shell.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,80 @@
```mermaid
flowchart LR
subgraph path2 [Path]
2["Path<br>[81, 109, 0]"]
3["Segment<br>[117, 136, 0]"]
4["Segment<br>[144, 164, 0]"]
5["Segment<br>[172, 192, 0]"]
6["Segment<br>[200, 207, 0]"]
7[Solid2d]
end
1["Plane<br>[56, 73, 0]"]
8["Sweep Extrusion<br>[215, 234, 0]"]
9[Wall]
10[Wall]
11[Wall]
12[Wall]
13["Cap Start"]
14["Cap End"]
15["SweepEdge Opposite"]
16["SweepEdge Opposite"]
17["SweepEdge Opposite"]
18["SweepEdge Opposite"]
19["SweepEdge Adjacent"]
20["SweepEdge Adjacent"]
21["SweepEdge Adjacent"]
22["SweepEdge Adjacent"]
1 --- 2
2 --- 3
2 --- 4
2 --- 5
2 --- 6
2 --- 7
2 ---- 8
3 --- 12
3 x--> 13
3 --- 17
3 --- 20
4 --- 10
4 x--> 13
4 --- 18
4 --- 21
5 --- 9
5 x--> 13
5 --- 15
5 --- 22
6 --- 11
6 x--> 13
6 --- 16
6 --- 19
8 --- 9
8 --- 10
8 --- 11
8 --- 12
8 --- 13
8 --- 14
8 --- 15
8 --- 16
8 --- 17
8 --- 18
8 --- 19
8 --- 20
8 --- 21
8 --- 22
15 <--x 9
21 <--x 9
22 <--x 9
18 <--x 10
20 <--x 10
21 <--x 10
16 <--x 11
19 <--x 11
22 <--x 11
17 <--x 12
19 <--x 12
20 <--x 12
15 <--x 14
16 <--x 14
17 <--x 14
18 <--x 14
```

View File

@ -0,0 +1,732 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Result of parsing clone_w_shell.kcl
---
{
"Ok": {
"body": [
{
"commentStart": 0,
"declaration": {
"commentStart": 0,
"end": 0,
"id": {
"commentStart": 0,
"end": 0,
"name": "firstSketch",
"start": 0,
"type": "Identifier"
},
"init": {
"body": [
{
"arguments": [
{
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "XY",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name",
"type": "Name"
}
],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "startSketchOn",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "at",
"start": 0,
"type": "Identifier"
},
"arg": {
"commentStart": 0,
"elements": [
{
"argument": {
"commentStart": 0,
"end": 0,
"raw": "12",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 12.0,
"suffix": "None"
}
},
"commentStart": 0,
"end": 0,
"operator": "-",
"start": 0,
"type": "UnaryExpression",
"type": "UnaryExpression"
},
{
"commentStart": 0,
"end": 0,
"raw": "12",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 12.0,
"suffix": "None"
}
}
],
"end": 0,
"start": 0,
"type": "ArrayExpression",
"type": "ArrayExpression"
}
}
],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "startProfile",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": null
},
{
"arguments": [
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "end",
"start": 0,
"type": "Identifier"
},
"arg": {
"commentStart": 0,
"elements": [
{
"commentStart": 0,
"end": 0,
"raw": "24",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 24.0,
"suffix": "None"
}
},
{
"commentStart": 0,
"end": 0,
"raw": "0",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 0.0,
"suffix": "None"
}
}
],
"end": 0,
"start": 0,
"type": "ArrayExpression",
"type": "ArrayExpression"
}
}
],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "line",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": null
},
{
"arguments": [
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "end",
"start": 0,
"type": "Identifier"
},
"arg": {
"commentStart": 0,
"elements": [
{
"commentStart": 0,
"end": 0,
"raw": "0",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 0.0,
"suffix": "None"
}
},
{
"argument": {
"commentStart": 0,
"end": 0,
"raw": "24",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 24.0,
"suffix": "None"
}
},
"commentStart": 0,
"end": 0,
"operator": "-",
"start": 0,
"type": "UnaryExpression",
"type": "UnaryExpression"
}
],
"end": 0,
"start": 0,
"type": "ArrayExpression",
"type": "ArrayExpression"
}
}
],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "line",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": null
},
{
"arguments": [
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "end",
"start": 0,
"type": "Identifier"
},
"arg": {
"commentStart": 0,
"elements": [
{
"argument": {
"commentStart": 0,
"end": 0,
"raw": "24",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 24.0,
"suffix": "None"
}
},
"commentStart": 0,
"end": 0,
"operator": "-",
"start": 0,
"type": "UnaryExpression",
"type": "UnaryExpression"
},
{
"commentStart": 0,
"end": 0,
"raw": "0",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 0.0,
"suffix": "None"
}
}
],
"end": 0,
"start": 0,
"type": "ArrayExpression",
"type": "ArrayExpression"
}
}
],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "line",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": null
},
{
"arguments": [],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "close",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "length",
"start": 0,
"type": "Identifier"
},
"arg": {
"commentStart": 0,
"end": 0,
"raw": "6",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 6.0,
"suffix": "None"
}
}
}
],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "extrude",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": null
}
],
"commentStart": 0,
"end": 0,
"nonCodeMeta": {
"nonCodeNodes": {
"6": [
{
"commentStart": 0,
"end": 0,
"start": 0,
"type": "NonCodeNode",
"value": {
"type": "newLineBlockComment",
"value": "Remove the end face for the extrusion.",
"style": "line"
}
}
]
},
"startNodes": []
},
"start": 0,
"type": "PipeExpression",
"type": "PipeExpression"
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 0,
"kind": "const",
"preComments": [
"// Remove the end face for the extrusion."
],
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"commentStart": 0,
"declaration": {
"commentStart": 0,
"end": 0,
"id": {
"commentStart": 0,
"end": 0,
"name": "firstShell",
"start": 0,
"type": "Identifier"
},
"init": {
"arguments": [
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "faces",
"start": 0,
"type": "Identifier"
},
"arg": {
"commentStart": 0,
"elements": [
{
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "END",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name",
"type": "Name"
}
],
"end": 0,
"start": 0,
"type": "ArrayExpression",
"type": "ArrayExpression"
}
},
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "thickness",
"start": 0,
"type": "Identifier"
},
"arg": {
"commentStart": 0,
"end": 0,
"raw": "0.25",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 0.25,
"suffix": "None"
}
}
}
],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "shell",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "firstSketch",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name",
"type": "Name"
}
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 0,
"kind": "const",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"commentStart": 0,
"declaration": {
"commentStart": 0,
"end": 0,
"id": {
"commentStart": 0,
"end": 0,
"name": "secondShell",
"start": 0,
"type": "Identifier"
},
"init": {
"body": [
{
"arguments": [
{
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "firstShell",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name",
"type": "Name"
}
],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "clone",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "x",
"start": 0,
"type": "Identifier"
},
"arg": {
"commentStart": 0,
"end": 0,
"raw": "50",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 50.0,
"suffix": "None"
}
}
}
],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "translate",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": null
}
],
"commentStart": 0,
"end": 0,
"start": 0,
"type": "PipeExpression",
"type": "PipeExpression"
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 0,
"kind": "const",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"commentStart": 0,
"end": 0,
"nonCodeMeta": {
"nonCodeNodes": {
"1": [
{
"commentStart": 0,
"end": 0,
"start": 0,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
}
],
"2": [
{
"commentStart": 0,
"end": 0,
"start": 0,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
}
]
},
"startNodes": []
},
"start": 0
}
}

View File

@ -0,0 +1,19 @@
// Remove the end face for the extrusion.
firstSketch = startSketchOn(XY)
|> startProfile(at = [-12, 12])
|> line(end = [24, 0])
|> line(end = [0, -24])
|> line(end = [-24, 0])
|> close()
|> extrude(length = 6)
// Remove the end face for the extrusion.
firstShell = shell(
firstSketch,
faces = [END],
thickness = 0.25,
)
secondShell = clone(firstShell)
|> translate(x = 50)

View File

@ -0,0 +1,119 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Operations executed clone_w_shell.kcl
---
[
{
"labeledArgs": {
"planeOrSolid": {
"value": {
"type": "Plane",
"artifact_id": "[uuid]"
},
"sourceRange": []
}
},
"name": "startSketchOn",
"sourceRange": [],
"type": "StdLibCall",
"unlabeledArg": null
},
{
"labeledArgs": {
"length": {
"value": {
"type": "Number",
"value": 6.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"sourceRange": []
}
},
"name": "extrude",
"sourceRange": [],
"type": "StdLibCall",
"unlabeledArg": {
"value": {
"type": "Sketch",
"value": {
"artifactId": "[uuid]"
}
},
"sourceRange": []
}
},
{
"type": "KclStdLibCall",
"name": "shell",
"unlabeledArg": {
"value": {
"type": "Array",
"value": [
{
"type": "Solid",
"value": {
"artifactId": "[uuid]"
}
}
]
},
"sourceRange": []
},
"labeledArgs": {
"faces": {
"value": {
"type": "Array",
"value": [
{
"type": "String",
"value": "end"
}
]
},
"sourceRange": []
},
"thickness": {
"value": {
"type": "Number",
"value": 0.25,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"sourceRange": []
}
},
"sourceRange": []
},
{
"labeledArgs": {
"geometry": {
"value": {
"type": "Solid",
"value": {
"artifactId": "[uuid]"
}
},
"sourceRange": []
}
},
"name": "clone",
"sourceRange": [],
"type": "StdLibCall",
"unlabeledArg": null
}
]

View File

@ -0,0 +1,517 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Variables in memory after executing clone_w_shell.kcl
---
{
"firstShell": {
"type": "Solid",
"value": {
"type": "Solid",
"id": "[uuid]",
"artifactId": "[uuid]",
"value": [
{
"faceId": "[uuid]",
"id": "[uuid]",
"sourceRange": [],
"tag": null,
"type": "extrudePlane"
},
{
"faceId": "[uuid]",
"id": "[uuid]",
"sourceRange": [],
"tag": null,
"type": "extrudePlane"
},
{
"faceId": "[uuid]",
"id": "[uuid]",
"sourceRange": [],
"tag": null,
"type": "extrudePlane"
},
{
"faceId": "[uuid]",
"id": "[uuid]",
"sourceRange": [],
"tag": null,
"type": "extrudePlane"
}
],
"sketch": {
"type": "Sketch",
"id": "[uuid]",
"paths": [
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
-12.0,
12.0
],
"tag": null,
"to": [
12.0,
12.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
12.0,
12.0
],
"tag": null,
"to": [
12.0,
-12.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
12.0,
-12.0
],
"tag": null,
"to": [
-12.0,
-12.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
-12.0,
-12.0
],
"tag": null,
"to": [
-12.0,
12.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
}
],
"on": {
"type": "plane",
"id": "[uuid]",
"artifactId": "[uuid]",
"value": "XY",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"xAxis": {
"x": 1.0,
"y": 0.0,
"z": 0.0,
"units": {
"type": "Unknown"
}
},
"yAxis": {
"x": 0.0,
"y": 1.0,
"z": 0.0,
"units": {
"type": "Unknown"
}
}
},
"start": {
"from": [
-12.0,
12.0
],
"to": [
-12.0,
12.0
],
"units": {
"type": "Mm"
},
"tag": null,
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
}
},
"height": 6.0,
"startCapId": "[uuid]",
"endCapId": "[uuid]",
"units": {
"type": "Mm"
},
"sectional": false
}
},
"firstSketch": {
"type": "Solid",
"value": {
"type": "Solid",
"id": "[uuid]",
"artifactId": "[uuid]",
"value": [
{
"faceId": "[uuid]",
"id": "[uuid]",
"sourceRange": [],
"tag": null,
"type": "extrudePlane"
},
{
"faceId": "[uuid]",
"id": "[uuid]",
"sourceRange": [],
"tag": null,
"type": "extrudePlane"
},
{
"faceId": "[uuid]",
"id": "[uuid]",
"sourceRange": [],
"tag": null,
"type": "extrudePlane"
},
{
"faceId": "[uuid]",
"id": "[uuid]",
"sourceRange": [],
"tag": null,
"type": "extrudePlane"
}
],
"sketch": {
"type": "Sketch",
"id": "[uuid]",
"paths": [
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
-12.0,
12.0
],
"tag": null,
"to": [
12.0,
12.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
12.0,
12.0
],
"tag": null,
"to": [
12.0,
-12.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
12.0,
-12.0
],
"tag": null,
"to": [
-12.0,
-12.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
-12.0,
-12.0
],
"tag": null,
"to": [
-12.0,
12.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
}
],
"on": {
"type": "plane",
"id": "[uuid]",
"artifactId": "[uuid]",
"value": "XY",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"xAxis": {
"x": 1.0,
"y": 0.0,
"z": 0.0,
"units": {
"type": "Unknown"
}
},
"yAxis": {
"x": 0.0,
"y": 1.0,
"z": 0.0,
"units": {
"type": "Unknown"
}
}
},
"start": {
"from": [
-12.0,
12.0
],
"to": [
-12.0,
12.0
],
"units": {
"type": "Mm"
},
"tag": null,
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
}
},
"height": 6.0,
"startCapId": "[uuid]",
"endCapId": "[uuid]",
"units": {
"type": "Mm"
},
"sectional": false
}
},
"secondShell": {
"type": "Solid",
"value": {
"type": "Solid",
"id": "[uuid]",
"artifactId": "[uuid]",
"value": [],
"sketch": {
"type": "Sketch",
"id": "[uuid]",
"paths": [
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
-12.0,
12.0
],
"tag": null,
"to": [
12.0,
12.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
12.0,
12.0
],
"tag": null,
"to": [
12.0,
-12.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
12.0,
-12.0
],
"tag": null,
"to": [
-12.0,
-12.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
-12.0,
-12.0
],
"tag": null,
"to": [
-12.0,
12.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
}
],
"on": {
"type": "plane",
"id": "[uuid]",
"artifactId": "[uuid]",
"value": "XY",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"xAxis": {
"x": 1.0,
"y": 0.0,
"z": 0.0,
"units": {
"type": "Unknown"
}
},
"yAxis": {
"x": 0.0,
"y": 1.0,
"z": 0.0,
"units": {
"type": "Unknown"
}
}
},
"start": {
"from": [
-12.0,
12.0
],
"to": [
-12.0,
12.0
],
"units": {
"type": "Mm"
},
"tag": null,
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
}
},
"height": 6.0,
"startCapId": null,
"endCapId": null,
"units": {
"type": "Mm"
},
"sectional": false
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -0,0 +1,18 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Result of unparsing clone_w_shell.kcl
---
// Remove the end face for the extrusion.
firstSketch = startSketchOn(XY)
|> startProfile(at = [-12, 12])
|> line(end = [24, 0])
|> line(end = [0, -24])
|> line(end = [-24, 0])
|> close()
|> extrude(length = 6)
// Remove the end face for the extrusion.
firstShell = shell(firstSketch, faces = [END], thickness = 0.25)
secondShell = clone(firstShell)
|> translate(x = 50)

View File

@ -5,7 +5,12 @@ import type {
LanguageServerClient,
LanguageServerOptions,
} from '@kittycad/codemirror-lsp-client'
import { lspFormatCodeEvent, lspPlugin } from '@kittycad/codemirror-lsp-client'
import {
lspCodeActionEvent,
lspFormatCodeEvent,
lspPlugin,
lspRenameEvent,
} from '@kittycad/codemirror-lsp-client'
import { updateOutsideEditorEvent } from '@src/editor/manager'
import { codeManagerUpdateEvent } from '@src/lang/codeManager'
import { codeManager, editorManager, kclManager } from '@src/lib/singletons'
@ -90,6 +95,16 @@ export class KclPlugin implements PluginValue {
// We want to ignore other events outside the editor.
isRelevant = false
break
} else if (tr.annotation(lspRenameEvent.type)) {
// Rename does not need to trigger the world.
// It's the same ast just different variable names.
isRelevant = false
break
} else if (tr.annotation(lspCodeActionEvent.type)) {
// Code actions should be stable enough where they create the same
// code and we do not need to need trigger an update.
isRelevant = false
break
}
}

View File

@ -206,6 +206,11 @@ code {
outline: none;
}
.cm-rename-popup {
/* we want to overpower anything else */
z-index: 99999999999 !important;
}
@keyframes blink {
0%,
100% {
@ -220,18 +225,22 @@ code {
@apply bg-transparent !important;
}
#code-mirror-override .cm-tooltip {
#code-mirror-override .cm-tooltip,
.cm-tooltip {
@apply text-xs shadow-md;
@apply bg-chalkboard-10 text-chalkboard-80;
@apply rounded-sm border-solid border border-chalkboard-40/30 border-l-liquid-10;
}
.dark #code-mirror-override .cm-tooltip {
.dark #code-mirror-override .cm-tooltip,
.cm-tooltip {
@apply bg-chalkboard-110 text-chalkboard-40;
@apply border-chalkboard-70/20 border-l-liquid-70;
}
#code-mirror-override .cm-tooltip-hover {
#code-mirror-override .cm-tooltip-hover,
#code-mirror-override .cm-signature-tooltip,
.cm-signature-tooltip {
@apply py-1 px-2 w-max max-w-md;
}

View File

@ -1,6 +1,9 @@
import type { Diagnostic as CodeMirrorDiagnostic } from '@codemirror/lint'
import type { Text } from '@codemirror/state'
import { posToOffset } from '@kittycad/codemirror-lsp-client'
import {
lspCodeActionEvent,
posToOffset,
} from '@kittycad/codemirror-lsp-client'
import type { EditorView } from 'codemirror'
import type { Diagnostic as LspDiagnostic } from 'vscode-languageserver-protocol'
@ -343,6 +346,7 @@ export function compilationErrorsToDiagnostics(
to: suggestion.source_range[1],
insert: suggestion.insert,
},
annotations: [lspCodeActionEvent],
})
},
},

View File

@ -1,5 +1,5 @@
import type { Diagnostic } from '@codemirror/lint'
import { lspCodeActionEvent } from '@kittycad/codemirror-lsp-client'
import type { Node } from '@rust/kcl-lib/bindings/Node'
import { KCLError } from '@src/lang/errors'
@ -8,6 +8,7 @@ import { emptyExecState, kclLint } from '@src/lang/wasm'
import { EXECUTE_AST_INTERRUPT_ERROR_STRING } from '@src/lib/constants'
import type RustContext from '@src/lib/rustContext'
import { jsAppSettings } from '@src/lib/settings/settingsUtils'
import type { EditorView } from 'codemirror'
export type ToolTip =
| 'lineTo'
@ -143,11 +144,31 @@ export async function lintAst({
try {
const discovered_findings = await kclLint(ast)
return discovered_findings.map((lint) => {
let actions
const suggestion = lint.suggestion
if (suggestion) {
actions = [
{
name: suggestion.title,
apply: (view: EditorView, from: number, to: number) => {
view.dispatch({
changes: {
from: suggestion.source_range[0],
to: suggestion.source_range[1],
insert: suggestion.insert,
},
annotations: [lspCodeActionEvent],
})
},
},
]
}
return {
message: lint.finding.title,
severity: 'info',
from: lint.pos[0],
to: lint.pos[1],
message: lint.finding.title,
severity: 'info',
actions,
}
})
} catch (e: any) {