2024-02-23 17:37:05 -05:00
|
|
|
import { undo, redo } from '@codemirror/commands'
|
2023-09-09 01:38:36 -04:00
|
|
|
import ReactCodeMirror, {
|
|
|
|
Extension,
|
|
|
|
ViewUpdate,
|
|
|
|
keymap,
|
|
|
|
} from '@uiw/react-codemirror'
|
2024-02-19 12:33:16 -08:00
|
|
|
import { FromServer, IntoServer } from 'editor/plugins/lsp/codec'
|
|
|
|
import Server from '../editor/plugins/lsp/server'
|
|
|
|
import Client from '../editor/plugins/lsp/client'
|
2023-09-09 01:38:36 -04:00
|
|
|
import { TEST } from 'env'
|
|
|
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
2024-02-16 09:09:58 -05:00
|
|
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
2023-09-09 01:38:36 -04:00
|
|
|
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
|
|
|
import { Themes } from 'lib/theme'
|
2024-02-26 21:02:33 +11:00
|
|
|
import { useEffect, useMemo, useRef } from 'react'
|
2023-09-09 01:38:36 -04:00
|
|
|
import { linter, lintGutter } from '@codemirror/lint'
|
2023-10-16 21:20:05 +11:00
|
|
|
import { useStore } from 'useStore'
|
|
|
|
import { processCodeMirrorRanges } from 'lib/selections'
|
2024-02-19 12:33:16 -08:00
|
|
|
import { LanguageServerClient } from 'editor/plugins/lsp'
|
|
|
|
import kclLanguage from 'editor/plugins/lsp/kcl/language'
|
2023-11-06 11:49:13 +11:00
|
|
|
import { EditorView, lineHighlightField } from 'editor/highlightextension'
|
2023-10-17 20:08:17 +11:00
|
|
|
import { roundOff } from 'lib/utils'
|
2023-09-09 01:38:36 -04:00
|
|
|
import { kclErrToDiagnostic } from 'lang/errors'
|
|
|
|
import { CSSRuleObject } from 'tailwindcss/types/config'
|
2023-10-11 13:36:54 +11:00
|
|
|
import { useModelingContext } from 'hooks/useModelingContext'
|
2023-09-14 00:03:51 -04:00
|
|
|
import interact from '@replit/codemirror-interact'
|
2023-09-25 19:49:53 -07:00
|
|
|
import { engineCommandManager } from '../lang/std/engineConnection'
|
2024-02-11 12:59:00 +11:00
|
|
|
import { kclManager, useKclContext } from 'lang/KclSingleton'
|
2024-02-23 11:24:22 -05:00
|
|
|
import { useFileContext } from 'hooks/useFileContext'
|
2024-02-11 12:59:00 +11:00
|
|
|
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
2024-02-14 08:03:20 +11:00
|
|
|
import { sceneInfra } from 'clientSideScene/sceneInfra'
|
2024-02-19 12:33:16 -08:00
|
|
|
import { copilotPlugin } from 'editor/plugins/lsp/copilot'
|
|
|
|
import { isTauri } from 'lib/isTauri'
|
|
|
|
import type * as LSP from 'vscode-languageserver-protocol'
|
2024-02-26 21:02:33 +11:00
|
|
|
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
|
2024-02-23 17:37:05 -05:00
|
|
|
import { useHotkeys } from 'react-hotkeys-hook'
|
2023-09-09 01:38:36 -04:00
|
|
|
|
|
|
|
export const editorShortcutMeta = {
|
|
|
|
formatCode: {
|
|
|
|
codeMirror: 'Alt-Shift-f',
|
|
|
|
display: 'Alt + Shift + F',
|
|
|
|
},
|
|
|
|
convertToVariable: {
|
|
|
|
codeMirror: 'Ctrl-Shift-c',
|
|
|
|
display: 'Ctrl + Shift + C',
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2024-02-19 12:33:16 -08:00
|
|
|
function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
|
|
|
|
// We only use workspace folders in Tauri since that is where we use more than
|
|
|
|
// one file.
|
|
|
|
if (isTauri()) {
|
|
|
|
return [{ uri: 'file://', name: 'ProjectRoot' }]
|
|
|
|
}
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
|
2023-09-09 01:38:36 -04:00
|
|
|
export const TextEditor = ({
|
|
|
|
theme,
|
|
|
|
}: {
|
|
|
|
theme: Themes.Light | Themes.Dark
|
|
|
|
}) => {
|
2023-12-01 20:18:51 +11:00
|
|
|
const {
|
|
|
|
editorView,
|
2024-02-15 13:56:31 -08:00
|
|
|
isKclLspServerReady,
|
|
|
|
isCopilotLspServerReady,
|
2023-12-01 20:18:51 +11:00
|
|
|
setEditorView,
|
2024-02-15 13:56:31 -08:00
|
|
|
setIsKclLspServerReady,
|
|
|
|
setIsCopilotLspServerReady,
|
2023-12-01 20:18:51 +11:00
|
|
|
isShiftDown,
|
|
|
|
} = useStore((s) => ({
|
|
|
|
editorView: s.editorView,
|
2024-02-15 13:56:31 -08:00
|
|
|
isKclLspServerReady: s.isKclLspServerReady,
|
|
|
|
isCopilotLspServerReady: s.isCopilotLspServerReady,
|
2023-12-01 20:18:51 +11:00
|
|
|
setEditorView: s.setEditorView,
|
2024-02-15 13:56:31 -08:00
|
|
|
setIsKclLspServerReady: s.setIsKclLspServerReady,
|
|
|
|
setIsCopilotLspServerReady: s.setIsCopilotLspServerReady,
|
2023-12-01 20:18:51 +11:00
|
|
|
isShiftDown: s.isShiftDown,
|
|
|
|
}))
|
2023-10-11 13:36:54 +11:00
|
|
|
const { code, errors } = useKclContext()
|
2024-02-11 12:59:00 +11:00
|
|
|
const lastEvent = useRef({ event: '', time: Date.now() })
|
2024-02-26 21:02:33 +11:00
|
|
|
const { overallState } = useNetworkStatus()
|
|
|
|
const isNetworkOkay = overallState === NetworkHealthState.Ok
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (typeof window === 'undefined') return
|
|
|
|
const onlineCallback = () => kclManager.setCodeAndExecute(kclManager.code)
|
|
|
|
window.addEventListener('online', onlineCallback)
|
|
|
|
return () => window.removeEventListener('online', onlineCallback)
|
|
|
|
}, [])
|
2023-09-09 01:38:36 -04:00
|
|
|
|
2024-02-23 17:37:05 -05:00
|
|
|
useHotkeys('mod+z', (e) => {
|
|
|
|
e.preventDefault()
|
|
|
|
if (editorView) {
|
|
|
|
undo(editorView)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
useHotkeys('mod+shift+z', (e) => {
|
|
|
|
e.preventDefault()
|
|
|
|
if (editorView) {
|
|
|
|
redo(editorView)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2023-09-09 01:38:36 -04:00
|
|
|
const {
|
2023-10-11 13:36:54 +11:00
|
|
|
context: { selectionRanges, selectionRangeTypeMap },
|
|
|
|
send,
|
2024-02-11 12:59:00 +11:00
|
|
|
state,
|
2023-10-11 13:36:54 +11:00
|
|
|
} = useModelingContext()
|
|
|
|
|
2024-02-15 13:56:31 -08:00
|
|
|
const { settings: { context: { textWrapping } = {} } = {}, auth } =
|
2024-02-16 09:09:58 -05:00
|
|
|
useGlobalStateContext()
|
Command bar: add extrude command, nonlinear editing, etc (#1204)
* Tweak toaster look and feel
* Add icons, tweak plus icon names
* Rename commandBarMeta to commandBarConfig
* Refactor command bar, add support for icons
* Create a tailwind plugin for aria-pressed button state
* Remove overlay from behind command bar
* Clean up toolbar
* Button and other style tweaks
* Icon tweaks follow-up: make old icons work with new sizing
* Delete unused static icons
* More CSS tweaks
* Small CSS tweak to project sidebar
* Add command bar E2E test
* fumpt
* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)
* fix typo in a comment
* Fix icon padding (built version only)
* Update onboarding and warning banner icons padding
* Misc minor style fixes
* Get Extrude opening and canceling from command bar
* Iconography tweaks
* Get extrude kind of working
* Refactor command bar config types and organization
* Move command bar configs to be co-located with each other
* Start building a state machine for the command bar
* Start converting command bar to state machine
* Add support for multiple args, confirmation step
* Submission behavior, hotkeys, code organization
* Add new test for extruding from command bar
* Polish step back and selection hotkeys, CSS tweaks
* Loading style tweaks
* Validate selection inputs, polish UX of args re-editing
* Prevent submission with multiple selection on singlular arg
* Remove stray console logs
* Tweak test, CSS nit, remove extrude "result" argument
* Fix linting warnings
* Show Ctrl+/ instead of ⌘K on all platforms but Mac
* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)
* Add "Enter sketch" to command bar
* fix command bar test
* Fix flaky cmd bar extrude test by waiting for engine select response
* Cover both button labels '⌘K' and 'Ctrl+/' in test
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-12-13 12:49:01 -05:00
|
|
|
const { commandBarSend } = useCommandsContext()
|
2024-02-23 11:24:22 -05:00
|
|
|
const {
|
|
|
|
context: { project },
|
|
|
|
} = useFileContext()
|
2023-09-09 01:38:36 -04:00
|
|
|
const { enable: convertEnabled, handleClick: convertCallback } =
|
|
|
|
useConvertToVariable()
|
|
|
|
|
|
|
|
// So this is a bit weird, we need to initialize the lsp server and client.
|
|
|
|
// But the server happens async so we break this into two parts.
|
|
|
|
// Below is the client and server promise.
|
2024-02-15 13:56:31 -08:00
|
|
|
const { lspClient: kclLspClient } = useMemo(() => {
|
2023-09-09 01:38:36 -04:00
|
|
|
const intoServer: IntoServer = new IntoServer()
|
|
|
|
const fromServer: FromServer = FromServer.create()
|
|
|
|
const client = new Client(fromServer, intoServer)
|
|
|
|
if (!TEST) {
|
2024-02-11 12:59:00 +11:00
|
|
|
Server.initialize(intoServer, fromServer).then((lspServer) => {
|
2024-02-15 13:56:31 -08:00
|
|
|
lspServer.start('kcl')
|
|
|
|
setIsKclLspServerReady(true)
|
2024-02-11 12:59:00 +11:00
|
|
|
})
|
2023-09-09 01:38:36 -04:00
|
|
|
}
|
|
|
|
|
2024-02-19 12:33:16 -08:00
|
|
|
const lspClient = new LanguageServerClient({ client, name: 'kcl' })
|
2023-09-09 01:38:36 -04:00
|
|
|
return { lspClient }
|
2024-02-15 13:56:31 -08:00
|
|
|
}, [setIsKclLspServerReady])
|
2023-09-09 01:38:36 -04:00
|
|
|
|
|
|
|
// Here we initialize the plugin which will start the client.
|
2024-02-23 11:24:22 -05:00
|
|
|
// Now that we have multi-file support the name of the file is a dep of
|
2023-09-09 01:38:36 -04:00
|
|
|
// this use memo, as well as the directory structure, which I think is
|
2023-11-01 17:34:54 -05:00
|
|
|
// a good setup because it will restart the client but not the server :)
|
2023-09-09 01:38:36 -04:00
|
|
|
// We do not want to restart the server, its just wasteful.
|
|
|
|
const kclLSP = useMemo(() => {
|
|
|
|
let plugin = null
|
2024-02-15 13:56:31 -08:00
|
|
|
if (isKclLspServerReady && !TEST) {
|
2023-09-09 01:38:36 -04:00
|
|
|
// Set up the lsp plugin.
|
|
|
|
const lsp = kclLanguage({
|
|
|
|
// When we have more than one file, we'll need to change this.
|
|
|
|
documentUri: `file:///we-just-have-one-file-for-now.kcl`,
|
2024-02-19 12:33:16 -08:00
|
|
|
workspaceFolders: getWorkspaceFolders(),
|
2024-02-15 13:56:31 -08:00
|
|
|
client: kclLspClient,
|
2023-09-09 01:38:36 -04:00
|
|
|
})
|
|
|
|
|
|
|
|
plugin = lsp
|
|
|
|
}
|
|
|
|
return plugin
|
2024-02-15 13:56:31 -08:00
|
|
|
}, [kclLspClient, isKclLspServerReady])
|
|
|
|
|
|
|
|
const { lspClient: copilotLspClient } = useMemo(() => {
|
|
|
|
const intoServer: IntoServer = new IntoServer()
|
|
|
|
const fromServer: FromServer = FromServer.create()
|
|
|
|
const client = new Client(fromServer, intoServer)
|
|
|
|
if (!TEST) {
|
|
|
|
Server.initialize(intoServer, fromServer).then((lspServer) => {
|
|
|
|
const token = auth?.context?.token
|
|
|
|
lspServer.start('copilot', token)
|
|
|
|
setIsCopilotLspServerReady(true)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-02-19 12:33:16 -08:00
|
|
|
const lspClient = new LanguageServerClient({ client, name: 'copilot' })
|
2024-02-15 13:56:31 -08:00
|
|
|
return { lspClient }
|
|
|
|
}, [setIsCopilotLspServerReady])
|
|
|
|
|
|
|
|
// Here we initialize the plugin which will start the client.
|
|
|
|
// When we have multi-file support the name of the file will be a dep of
|
|
|
|
// this use memo, as well as the directory structure, which I think is
|
|
|
|
// a good setup because it will restart the client but not the server :)
|
|
|
|
// We do not want to restart the server, its just wasteful.
|
|
|
|
const copilotLSP = useMemo(() => {
|
|
|
|
let plugin = null
|
|
|
|
if (isCopilotLspServerReady && !TEST) {
|
|
|
|
// Set up the lsp plugin.
|
2024-02-19 12:33:16 -08:00
|
|
|
const lsp = copilotPlugin({
|
2024-02-15 13:56:31 -08:00
|
|
|
// When we have more than one file, we'll need to change this.
|
|
|
|
documentUri: `file:///we-just-have-one-file-for-now.kcl`,
|
2024-02-19 12:33:16 -08:00
|
|
|
workspaceFolders: getWorkspaceFolders(),
|
2024-02-15 13:56:31 -08:00
|
|
|
client: copilotLspClient,
|
|
|
|
allowHTMLContent: true,
|
|
|
|
})
|
|
|
|
|
|
|
|
plugin = lsp
|
|
|
|
}
|
|
|
|
return plugin
|
2024-02-23 11:24:22 -05:00
|
|
|
}, [copilotLspClient, isCopilotLspServerReady, project])
|
2023-09-09 01:38:36 -04:00
|
|
|
|
|
|
|
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
2024-02-26 21:02:33 +11:00
|
|
|
const onChange = async (newCode: string) => {
|
|
|
|
if (isNetworkOkay) kclManager.setCodeAndExecute(newCode)
|
|
|
|
else kclManager.setCode(newCode)
|
2023-09-09 01:38:36 -04:00
|
|
|
} //, []);
|
|
|
|
const onUpdate = (viewUpdate: ViewUpdate) => {
|
|
|
|
if (!editorView) {
|
|
|
|
setEditorView(viewUpdate.view)
|
|
|
|
}
|
2024-02-14 08:03:20 +11:00
|
|
|
if (sceneInfra.selected) return // mid drag
|
2024-02-11 12:59:00 +11:00
|
|
|
const ignoreEvents: ModelingMachineEvent['type'][] = [
|
|
|
|
'Equip Line tool',
|
|
|
|
'Equip tangential arc to',
|
|
|
|
]
|
|
|
|
if (ignoreEvents.includes(state.event.type)) return
|
2023-10-16 21:20:05 +11:00
|
|
|
const eventInfo = processCodeMirrorRanges({
|
|
|
|
codeMirrorRanges: viewUpdate.state.selection.ranges,
|
|
|
|
selectionRanges,
|
|
|
|
selectionRangeTypeMap,
|
2023-12-01 20:18:51 +11:00
|
|
|
isShiftDown,
|
2023-09-09 01:38:36 -04:00
|
|
|
})
|
2023-10-16 21:20:05 +11:00
|
|
|
if (!eventInfo) return
|
2024-02-11 12:59:00 +11:00
|
|
|
const deterministicEventInfo = {
|
|
|
|
...eventInfo,
|
|
|
|
engineEvents: eventInfo.engineEvents.map((e) => ({
|
|
|
|
...e,
|
|
|
|
cmd_id: 'static',
|
|
|
|
})),
|
|
|
|
}
|
|
|
|
const stringEvent = JSON.stringify(deterministicEventInfo)
|
|
|
|
if (
|
|
|
|
stringEvent === lastEvent.current.event &&
|
|
|
|
Date.now() - lastEvent.current.time < 500
|
|
|
|
)
|
|
|
|
return // don't repeat events
|
|
|
|
lastEvent.current = { event: stringEvent, time: Date.now() }
|
2023-10-16 21:20:05 +11:00
|
|
|
send(eventInfo.modelingEvent)
|
|
|
|
eventInfo.engineEvents.forEach((event) =>
|
|
|
|
engineCommandManager.sendSceneCommand(event)
|
|
|
|
)
|
2023-09-09 01:38:36 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
const editorExtensions = useMemo(() => {
|
|
|
|
const extensions = [
|
|
|
|
lineHighlightField,
|
|
|
|
keymap.of([
|
|
|
|
{
|
|
|
|
key: 'Meta-k',
|
|
|
|
run: () => {
|
Command bar: add extrude command, nonlinear editing, etc (#1204)
* Tweak toaster look and feel
* Add icons, tweak plus icon names
* Rename commandBarMeta to commandBarConfig
* Refactor command bar, add support for icons
* Create a tailwind plugin for aria-pressed button state
* Remove overlay from behind command bar
* Clean up toolbar
* Button and other style tweaks
* Icon tweaks follow-up: make old icons work with new sizing
* Delete unused static icons
* More CSS tweaks
* Small CSS tweak to project sidebar
* Add command bar E2E test
* fumpt
* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)
* fix typo in a comment
* Fix icon padding (built version only)
* Update onboarding and warning banner icons padding
* Misc minor style fixes
* Get Extrude opening and canceling from command bar
* Iconography tweaks
* Get extrude kind of working
* Refactor command bar config types and organization
* Move command bar configs to be co-located with each other
* Start building a state machine for the command bar
* Start converting command bar to state machine
* Add support for multiple args, confirmation step
* Submission behavior, hotkeys, code organization
* Add new test for extruding from command bar
* Polish step back and selection hotkeys, CSS tweaks
* Loading style tweaks
* Validate selection inputs, polish UX of args re-editing
* Prevent submission with multiple selection on singlular arg
* Remove stray console logs
* Tweak test, CSS nit, remove extrude "result" argument
* Fix linting warnings
* Show Ctrl+/ instead of ⌘K on all platforms but Mac
* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)
* Add "Enter sketch" to command bar
* fix command bar test
* Fix flaky cmd bar extrude test by waiting for engine select response
* Cover both button labels '⌘K' and 'Ctrl+/' in test
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-12-13 12:49:01 -05:00
|
|
|
commandBarSend({ type: 'Open' })
|
2023-09-09 01:38:36 -04:00
|
|
|
return false
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
key: editorShortcutMeta.formatCode.codeMirror,
|
|
|
|
run: () => {
|
2023-10-11 13:36:54 +11:00
|
|
|
kclManager.format()
|
2023-09-09 01:38:36 -04:00
|
|
|
return true
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
key: editorShortcutMeta.convertToVariable.codeMirror,
|
|
|
|
run: () => {
|
|
|
|
if (convertEnabled) {
|
2024-02-11 12:59:00 +11:00
|
|
|
convertCallback()
|
2023-09-09 01:38:36 -04:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
},
|
|
|
|
},
|
|
|
|
]),
|
|
|
|
] as Extension[]
|
|
|
|
|
|
|
|
if (kclLSP) extensions.push(kclLSP)
|
2024-02-15 13:56:31 -08:00
|
|
|
if (copilotLSP) extensions.push(copilotLSP)
|
2023-09-09 01:38:36 -04:00
|
|
|
|
|
|
|
// These extensions have proven to mess with vitest
|
|
|
|
if (!TEST) {
|
|
|
|
extensions.push(
|
|
|
|
lintGutter(),
|
|
|
|
linter((_view) => {
|
2023-10-11 13:36:54 +11:00
|
|
|
return kclErrToDiagnostic(errors)
|
2023-09-14 00:03:51 -04:00
|
|
|
}),
|
|
|
|
interact({
|
|
|
|
rules: [
|
|
|
|
// a rule for a number dragger
|
|
|
|
{
|
|
|
|
// the regexp matching the value
|
|
|
|
regexp: /-?\b\d+\.?\d*\b/g,
|
|
|
|
// set cursor to "ew-resize" on hover
|
|
|
|
cursor: 'ew-resize',
|
|
|
|
// change number value based on mouse X movement on drag
|
|
|
|
onDrag: (text, setText, e) => {
|
|
|
|
const multiplier =
|
|
|
|
e.shiftKey && e.metaKey
|
|
|
|
? 0.01
|
|
|
|
: e.metaKey
|
|
|
|
? 0.1
|
|
|
|
: e.shiftKey
|
|
|
|
? 10
|
|
|
|
: 1
|
|
|
|
|
|
|
|
const delta = e.movementX * multiplier
|
|
|
|
|
|
|
|
const newVal = roundOff(
|
|
|
|
Number(text) + delta,
|
|
|
|
multiplier === 0.01 ? 2 : multiplier === 0.1 ? 1 : 0
|
|
|
|
)
|
|
|
|
|
|
|
|
if (isNaN(newVal)) return
|
|
|
|
setText(newVal.toString())
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
2023-09-09 01:38:36 -04:00
|
|
|
})
|
|
|
|
)
|
|
|
|
if (textWrapping === 'On') extensions.push(EditorView.lineWrapping)
|
|
|
|
}
|
|
|
|
|
|
|
|
return extensions
|
2023-10-14 03:47:46 +11:00
|
|
|
}, [kclLSP, textWrapping, convertCallback])
|
2023-09-09 01:38:36 -04:00
|
|
|
|
|
|
|
return (
|
|
|
|
<div
|
|
|
|
id="code-mirror-override"
|
|
|
|
className="full-height-subtract"
|
|
|
|
style={{ '--height-subtract': '4.25rem' } as CSSRuleObject}
|
|
|
|
>
|
|
|
|
<ReactCodeMirror
|
|
|
|
className="h-full"
|
|
|
|
value={code}
|
|
|
|
extensions={editorExtensions}
|
|
|
|
onChange={onChange}
|
|
|
|
onUpdate={onUpdate}
|
|
|
|
theme={theme}
|
|
|
|
onCreateEditor={(_editorView) => setEditorView(_editorView)}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|