lsp workspace stuff (#1677)
* some lsp shit Signed-off-by: Jess Frazelle <github@jessfraz.com> * more stuffs Signed-off-by: Jess Frazelle <github@jessfraz.com> * on open send close and open events Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * update the path 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> * send on close Signed-off-by: Jess Frazelle <github@jessfraz.com> * on close project Signed-off-by: Jess Frazelle <github@jessfraz.com> * update on close Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * initpromise Signed-off-by: Jess Frazelle <github@jessfraz.com> * add to wasm.ts Signed-off-by: Jess Frazelle <github@jessfraz.com> * Update src/lang/wasm.ts Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch> * restart lsps on failure Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * add panic hook Signed-off-by: Jess Frazelle <github@jessfraz.com> * updartes 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: Kurt Hutten <k.hutten@protonmail.ch>
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
import { useCallback, MouseEventHandler } from 'react'
|
||||
import { useCallback, MouseEventHandler, useEffect } from 'react'
|
||||
import { DebugPanel } from './components/DebugPanel'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { PaneType, useStore } from './useStore'
|
||||
@ -32,11 +32,17 @@ import { engineCommandManager } from './lang/std/engineConnection'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import { useLspContext } from 'components/LspProvider'
|
||||
|
||||
export function App() {
|
||||
const { project, file } = useLoaderData() as IndexLoaderData
|
||||
const navigate = useNavigate()
|
||||
const filePath = useAbsoluteFilePath()
|
||||
const { onProjectOpen } = useLspContext()
|
||||
|
||||
useEffect(() => {
|
||||
onProjectOpen(project || null, file || null)
|
||||
}, [])
|
||||
|
||||
useHotKeyListener()
|
||||
const {
|
||||
|
@ -38,6 +38,7 @@ import { sep } from '@tauri-apps/api/path'
|
||||
import { paths } from 'lib/paths'
|
||||
import { IndexLoaderData, HomeLoaderData } from 'lib/types'
|
||||
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
||||
import LspProvider from 'components/LspProvider'
|
||||
|
||||
export const BROWSER_FILE_NAME = 'new'
|
||||
|
||||
@ -52,7 +53,9 @@ const addGlobalContextToElements = (
|
||||
...route,
|
||||
element: (
|
||||
<CommandBarProvider>
|
||||
<SettingsAuthProvider>{route.element}</SettingsAuthProvider>
|
||||
<SettingsAuthProvider>
|
||||
<LspProvider>{route.element}</LspProvider>
|
||||
</SettingsAuthProvider>
|
||||
</CommandBarProvider>
|
||||
),
|
||||
}
|
||||
|
@ -15,11 +15,20 @@ import { FILE_EXT, sortProject } from 'lib/tauriFS'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
import { kclManager } from 'lang/KclSingleton'
|
||||
import { useDocumentHasFocus } from 'hooks/useDocumentHasFocus'
|
||||
import { useLspContext } from './LspProvider'
|
||||
|
||||
function getIndentationCSS(level: number) {
|
||||
return `calc(1rem * ${level + 1})`
|
||||
}
|
||||
|
||||
// an OS-agnostic way to get the basename of the path.
|
||||
export function basename(path: string): string {
|
||||
// Regular expression to match the last portion of the path, taking into account both POSIX and Windows delimiters
|
||||
const re = /[^\\/]+$/
|
||||
const match = path.match(re)
|
||||
return match ? match[0] : ''
|
||||
}
|
||||
|
||||
function RenameForm({
|
||||
fileOrDir,
|
||||
setIsRenaming,
|
||||
@ -147,6 +156,7 @@ const FileTreeItem = ({
|
||||
level?: number
|
||||
}) => {
|
||||
const { send, context } = useFileContext()
|
||||
const { lspClients } = useLspContext()
|
||||
const navigate = useNavigate()
|
||||
const [isRenaming, setIsRenaming] = useState(false)
|
||||
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
|
||||
@ -174,6 +184,28 @@ const FileTreeItem = ({
|
||||
kclManager.code
|
||||
)
|
||||
} else {
|
||||
// Let the lsp servers know we closed a file.
|
||||
const currentFilePath = basename(currentFile?.path || 'main.kcl')
|
||||
lspClients.forEach((lspClient) => {
|
||||
lspClient.textDocumentDidClose({
|
||||
textDocument: {
|
||||
uri: `file:///${currentFilePath}`,
|
||||
},
|
||||
})
|
||||
})
|
||||
const newFilePath = basename(fileOrDir.path)
|
||||
// Then let the clients know we opened a file.
|
||||
lspClients.forEach((lspClient) => {
|
||||
lspClient.textDocumentDidOpen({
|
||||
textDocument: {
|
||||
uri: `file:///${newFilePath}`,
|
||||
languageId: 'kcl',
|
||||
version: 1,
|
||||
text: '',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// Open kcl files
|
||||
navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`)
|
||||
}
|
||||
|
191
src/components/LspProvider.tsx
Normal file
191
src/components/LspProvider.tsx
Normal file
@ -0,0 +1,191 @@
|
||||
import { LanguageServerClient } from 'editor/plugins/lsp'
|
||||
import type * as LSP from 'vscode-languageserver-protocol'
|
||||
import React, { createContext, useMemo, useContext } from 'react'
|
||||
import { FromServer, IntoServer } from 'editor/plugins/lsp/codec'
|
||||
import Server from '../editor/plugins/lsp/server'
|
||||
import Client from '../editor/plugins/lsp/client'
|
||||
import { TEST } from 'env'
|
||||
import kclLanguage from 'editor/plugins/lsp/kcl/language'
|
||||
import { copilotPlugin } from 'editor/plugins/lsp/copilot'
|
||||
import { useStore } from 'useStore'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { Extension } from '@codemirror/state'
|
||||
import { LanguageSupport } from '@codemirror/language'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { basename } from './FileTree'
|
||||
import { paths } from 'lib/paths'
|
||||
import { FileEntry } from '@tauri-apps/api/fs'
|
||||
import { ProjectWithEntryPointMetadata } from 'lib/types'
|
||||
|
||||
function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
|
||||
return []
|
||||
}
|
||||
|
||||
type LspContext = {
|
||||
lspClients: LanguageServerClient[]
|
||||
copilotLSP: Extension | null
|
||||
kclLSP: LanguageSupport | null
|
||||
onProjectClose: (file: FileEntry | null, redirect: boolean) => void
|
||||
onProjectOpen: (
|
||||
project: ProjectWithEntryPointMetadata | null,
|
||||
file: FileEntry | null
|
||||
) => void
|
||||
}
|
||||
|
||||
export const LspStateContext = createContext({} as LspContext)
|
||||
export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const {
|
||||
isKclLspServerReady,
|
||||
isCopilotLspServerReady,
|
||||
setIsKclLspServerReady,
|
||||
setIsCopilotLspServerReady,
|
||||
} = useStore((s) => ({
|
||||
isKclLspServerReady: s.isKclLspServerReady,
|
||||
isCopilotLspServerReady: s.isCopilotLspServerReady,
|
||||
setIsKclLspServerReady: s.setIsKclLspServerReady,
|
||||
setIsCopilotLspServerReady: s.setIsCopilotLspServerReady,
|
||||
}))
|
||||
|
||||
const { auth } = useSettingsAuthContext()
|
||||
const navigate = useNavigate()
|
||||
|
||||
// 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.
|
||||
const { lspClient: kclLspClient } = 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) => {
|
||||
lspServer.start('kcl')
|
||||
setIsKclLspServerReady(true)
|
||||
})
|
||||
}
|
||||
|
||||
const lspClient = new LanguageServerClient({ client, name: 'kcl' })
|
||||
return { lspClient }
|
||||
}, [setIsKclLspServerReady])
|
||||
|
||||
// Here we initialize the plugin which will start the client.
|
||||
// Now that we have multi-file support the name of the file is 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 kclLSP = useMemo(() => {
|
||||
let plugin = null
|
||||
if (isKclLspServerReady && !TEST) {
|
||||
// Set up the lsp plugin.
|
||||
const lsp = kclLanguage({
|
||||
documentUri: `file:///main.kcl`,
|
||||
workspaceFolders: getWorkspaceFolders(),
|
||||
client: kclLspClient,
|
||||
})
|
||||
|
||||
plugin = lsp
|
||||
}
|
||||
return plugin
|
||||
}, [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)
|
||||
})
|
||||
}
|
||||
|
||||
const lspClient = new LanguageServerClient({ client, name: 'copilot' })
|
||||
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.
|
||||
const lsp = copilotPlugin({
|
||||
documentUri: `file:///main.kcl`,
|
||||
workspaceFolders: getWorkspaceFolders(),
|
||||
client: copilotLspClient,
|
||||
allowHTMLContent: true,
|
||||
})
|
||||
|
||||
plugin = lsp
|
||||
}
|
||||
return plugin
|
||||
}, [copilotLspClient, isCopilotLspServerReady])
|
||||
|
||||
const lspClients = [kclLspClient, copilotLspClient]
|
||||
|
||||
const onProjectClose = (file: FileEntry | null, redirect: boolean) => {
|
||||
const currentFilePath = basename(file?.name || 'main.kcl')
|
||||
lspClients.forEach((lspClient) => {
|
||||
lspClient.textDocumentDidClose({
|
||||
textDocument: {
|
||||
uri: `file:///${currentFilePath}`,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
if (redirect) {
|
||||
navigate(paths.HOME)
|
||||
}
|
||||
}
|
||||
|
||||
const onProjectOpen = (
|
||||
project: ProjectWithEntryPointMetadata | null,
|
||||
file: FileEntry | null
|
||||
) => {
|
||||
const projectName = project?.name || 'ProjectRoot'
|
||||
// Send that the workspace folders changed.
|
||||
lspClients.forEach((lspClient) => {
|
||||
lspClient.workspaceDidChangeWorkspaceFolders(
|
||||
[{ uri: 'file://', name: projectName }],
|
||||
[]
|
||||
)
|
||||
})
|
||||
if (file) {
|
||||
// Send that the file was opened.
|
||||
const filename = basename(file?.name || 'main.kcl')
|
||||
lspClients.forEach((lspClient) => {
|
||||
lspClient.textDocumentDidOpen({
|
||||
textDocument: {
|
||||
uri: `file:///${filename}`,
|
||||
languageId: 'kcl',
|
||||
version: 1,
|
||||
text: '',
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<LspStateContext.Provider
|
||||
value={{
|
||||
lspClients,
|
||||
copilotLSP,
|
||||
kclLSP,
|
||||
onProjectClose,
|
||||
onProjectOpen,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</LspStateContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default LspProvider
|
||||
|
||||
export const useLspContext = () => {
|
||||
return useContext(LspStateContext)
|
||||
}
|
@ -12,6 +12,7 @@ import { Logo } from './Logo'
|
||||
import { APP_NAME } from 'lib/constants'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
import { useLspContext } from './LspProvider'
|
||||
|
||||
const ProjectSidebarMenu = ({
|
||||
project,
|
||||
@ -22,14 +23,24 @@ const ProjectSidebarMenu = ({
|
||||
project?: IndexLoaderData['project']
|
||||
file?: IndexLoaderData['file']
|
||||
}) => {
|
||||
const { onProjectClose } = useLspContext()
|
||||
return (
|
||||
<div className="rounded-sm !no-underline h-9 mr-auto max-h-min min-w-max border-0 py-1 px-2 flex items-center gap-2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-energy-50 dark:hover:bg-chalkboard-90">
|
||||
<Link to={paths.HOME} className="group">
|
||||
<Link
|
||||
onClick={() => {
|
||||
onProjectClose(file || null, false)
|
||||
}}
|
||||
to={paths.HOME}
|
||||
className="group"
|
||||
>
|
||||
<Logo className="w-auto h-5 text-chalkboard-120 dark:text-chalkboard-10 group-hover:text-energy-10" />
|
||||
</Link>
|
||||
{renderAsLink ? (
|
||||
<>
|
||||
<Link
|
||||
onClick={() => {
|
||||
onProjectClose(file || null, false)
|
||||
}}
|
||||
to={paths.HOME}
|
||||
className="!no-underline"
|
||||
data-testid="project-sidebar-link"
|
||||
@ -57,6 +68,7 @@ function ProjectMenuPopover({
|
||||
file?: IndexLoaderData['file']
|
||||
}) {
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const { onProjectClose } = useLspContext()
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
@ -149,8 +161,10 @@ function ProjectMenuPopover({
|
||||
</ActionButton>
|
||||
{isTauri() && (
|
||||
<ActionButton
|
||||
Element="link"
|
||||
to={paths.HOME}
|
||||
Element="button"
|
||||
onClick={() => {
|
||||
onProjectClose(file || null, true)
|
||||
}}
|
||||
icon={{
|
||||
icon: faHome,
|
||||
className: 'p-1',
|
||||
|
@ -4,9 +4,6 @@ import ReactCodeMirror, {
|
||||
ViewUpdate,
|
||||
keymap,
|
||||
} from '@uiw/react-codemirror'
|
||||
import { FromServer, IntoServer } from 'editor/plugins/lsp/codec'
|
||||
import Server from '../editor/plugins/lsp/server'
|
||||
import Client from '../editor/plugins/lsp/client'
|
||||
import { TEST } from 'env'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
@ -16,8 +13,6 @@ import { useEffect, useMemo, useRef } from 'react'
|
||||
import { linter, lintGutter } from '@codemirror/lint'
|
||||
import { useStore } from 'useStore'
|
||||
import { processCodeMirrorRanges } from 'lib/selections'
|
||||
import { LanguageServerClient } from 'editor/plugins/lsp'
|
||||
import kclLanguage from 'editor/plugins/lsp/kcl/language'
|
||||
import { EditorView, lineHighlightField } from 'editor/highlightextension'
|
||||
import { roundOff } from 'lib/utils'
|
||||
import { kclErrToDiagnostic } from 'lang/errors'
|
||||
@ -26,14 +21,11 @@ import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import interact from '@replit/codemirror-interact'
|
||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
||||
import { kclManager, useKclContext } from 'lang/KclSingleton'
|
||||
import { useFileContext } from 'hooks/useFileContext'
|
||||
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
||||
import { sceneInfra } from 'clientSideScene/sceneInfra'
|
||||
import { copilotPlugin } from 'editor/plugins/lsp/copilot'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import type * as LSP from 'vscode-languageserver-protocol'
|
||||
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { useLspContext } from './LspProvider'
|
||||
|
||||
export const editorShortcutMeta = {
|
||||
formatCode: {
|
||||
@ -46,41 +38,21 @@ export const editorShortcutMeta = {
|
||||
},
|
||||
}
|
||||
|
||||
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 []
|
||||
}
|
||||
|
||||
export const TextEditor = ({
|
||||
theme,
|
||||
}: {
|
||||
theme: Themes.Light | Themes.Dark
|
||||
}) => {
|
||||
const {
|
||||
editorView,
|
||||
isKclLspServerReady,
|
||||
isCopilotLspServerReady,
|
||||
setEditorView,
|
||||
setIsKclLspServerReady,
|
||||
setIsCopilotLspServerReady,
|
||||
isShiftDown,
|
||||
} = useStore((s) => ({
|
||||
const { editorView, setEditorView, isShiftDown } = useStore((s) => ({
|
||||
editorView: s.editorView,
|
||||
isKclLspServerReady: s.isKclLspServerReady,
|
||||
isCopilotLspServerReady: s.isCopilotLspServerReady,
|
||||
setEditorView: s.setEditorView,
|
||||
setIsKclLspServerReady: s.setIsKclLspServerReady,
|
||||
setIsCopilotLspServerReady: s.setIsCopilotLspServerReady,
|
||||
isShiftDown: s.isShiftDown,
|
||||
}))
|
||||
const { code, errors } = useKclContext()
|
||||
const lastEvent = useRef({ event: '', time: Date.now() })
|
||||
const { overallState } = useNetworkStatus()
|
||||
const isNetworkOkay = overallState === NetworkHealthState.Ok
|
||||
const { copilotLSP, kclLSP } = useLspContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return
|
||||
@ -108,92 +80,12 @@ export const TextEditor = ({
|
||||
state,
|
||||
} = useModelingContext()
|
||||
|
||||
const { settings, auth } = useSettingsAuthContext()
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const textWrapping = settings.context?.textWrapping ?? 'On'
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const {
|
||||
context: { project },
|
||||
} = useFileContext()
|
||||
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.
|
||||
const { lspClient: kclLspClient } = 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) => {
|
||||
lspServer.start('kcl')
|
||||
setIsKclLspServerReady(true)
|
||||
})
|
||||
}
|
||||
|
||||
const lspClient = new LanguageServerClient({ client, name: 'kcl' })
|
||||
return { lspClient }
|
||||
}, [setIsKclLspServerReady])
|
||||
|
||||
// Here we initialize the plugin which will start the client.
|
||||
// Now that we have multi-file support the name of the file is 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 kclLSP = useMemo(() => {
|
||||
let plugin = null
|
||||
if (isKclLspServerReady && !TEST) {
|
||||
// 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`,
|
||||
workspaceFolders: getWorkspaceFolders(),
|
||||
client: kclLspClient,
|
||||
})
|
||||
|
||||
plugin = lsp
|
||||
}
|
||||
return plugin
|
||||
}, [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)
|
||||
})
|
||||
}
|
||||
|
||||
const lspClient = new LanguageServerClient({ client, name: 'copilot' })
|
||||
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.
|
||||
const lsp = copilotPlugin({
|
||||
// When we have more than one file, we'll need to change this.
|
||||
documentUri: `file:///we-just-have-one-file-for-now.kcl`,
|
||||
workspaceFolders: getWorkspaceFolders(),
|
||||
client: copilotLspClient,
|
||||
allowHTMLContent: true,
|
||||
})
|
||||
|
||||
plugin = lsp
|
||||
}
|
||||
return plugin
|
||||
}, [copilotLspClient, isCopilotLspServerReady, project])
|
||||
|
||||
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
||||
const onChange = async (newCode: string) => {
|
||||
if (isNetworkOkay) kclManager.setCodeAndExecute(newCode)
|
||||
|
@ -88,6 +88,8 @@ interface LSPNotifyMap {
|
||||
initialized: LSP.InitializedParams
|
||||
'textDocument/didChange': LSP.DidChangeTextDocumentParams
|
||||
'textDocument/didOpen': LSP.DidOpenTextDocumentParams
|
||||
'textDocument/didClose': LSP.DidCloseTextDocumentParams
|
||||
'workspace/didChangeWorkspaceFolders': LSP.DidChangeWorkspaceFoldersParams
|
||||
}
|
||||
|
||||
export interface LanguageServerClientOptions {
|
||||
@ -149,6 +151,12 @@ export class LanguageServerClient {
|
||||
textDocumentDidOpen(params: LSP.DidOpenTextDocumentParams) {
|
||||
this.notify('textDocument/didOpen', params)
|
||||
|
||||
// Update the facet of the plugins to the correct value.
|
||||
for (const plugin of this.plugins) {
|
||||
plugin.documentUri = params.textDocument.uri
|
||||
plugin.languageId = params.textDocument.languageId
|
||||
}
|
||||
|
||||
this.updateSemanticTokens(params.textDocument.uri)
|
||||
}
|
||||
|
||||
@ -157,6 +165,28 @@ export class LanguageServerClient {
|
||||
this.updateSemanticTokens(params.textDocument.uri)
|
||||
}
|
||||
|
||||
textDocumentDidClose(params: LSP.DidCloseTextDocumentParams) {
|
||||
this.notify('textDocument/didClose', params)
|
||||
}
|
||||
|
||||
workspaceDidChangeWorkspaceFolders(
|
||||
added: LSP.WorkspaceFolder[],
|
||||
removed: LSP.WorkspaceFolder[]
|
||||
) {
|
||||
// Add all the current workspace folders in the plugin to removed.
|
||||
for (const plugin of this.plugins) {
|
||||
removed.push(...plugin.workspaceFolders)
|
||||
}
|
||||
this.notify('workspace/didChangeWorkspaceFolders', {
|
||||
event: { added, removed },
|
||||
})
|
||||
|
||||
// Add all the new workspace folders to the plugins.
|
||||
for (const plugin of this.plugins) {
|
||||
plugin.workspaceFolders = added
|
||||
}
|
||||
}
|
||||
|
||||
async updateSemanticTokens(uri: string) {
|
||||
const serverCapabilities = this.getServerCapabilities()
|
||||
if (!serverCapabilities.semanticTokensProvider) {
|
||||
|
@ -37,9 +37,9 @@ const CompletionItemKindMap = Object.fromEntries(
|
||||
|
||||
export class LanguageServerPlugin implements PluginValue {
|
||||
public client: LanguageServerClient
|
||||
private documentUri: string
|
||||
private languageId: string
|
||||
private workspaceFolders: LSP.WorkspaceFolder[]
|
||||
public documentUri: string
|
||||
public languageId: string
|
||||
public workspaceFolders: LSP.WorkspaceFolder[]
|
||||
private documentVersion: number
|
||||
|
||||
constructor(
|
||||
|
@ -1,11 +1,7 @@
|
||||
import init, {
|
||||
copilot_lsp_run,
|
||||
InitOutput,
|
||||
kcl_lsp_run,
|
||||
ServerConfig,
|
||||
} from 'wasm-lib/pkg/wasm_lib'
|
||||
import { InitOutput, ServerConfig } from 'wasm-lib/pkg/wasm_lib'
|
||||
import { FromServer, IntoServer } from './codec'
|
||||
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
||||
import { copilotLspRun, initPromise, kclLspRun } from 'lang/wasm'
|
||||
|
||||
export default class Server {
|
||||
readonly initOutput: InitOutput
|
||||
@ -26,7 +22,7 @@ export default class Server {
|
||||
intoServer: IntoServer,
|
||||
fromServer: FromServer
|
||||
): Promise<Server> {
|
||||
const initOutput = await init()
|
||||
const initOutput = await initPromise
|
||||
const server = new Server(initOutput, intoServer, fromServer)
|
||||
return server
|
||||
}
|
||||
@ -38,12 +34,12 @@ export default class Server {
|
||||
fileSystemManager
|
||||
)
|
||||
if (type_ === 'copilot') {
|
||||
if (!token) {
|
||||
if (!token || token === '') {
|
||||
throw new Error('auth token is required for copilot')
|
||||
}
|
||||
await copilot_lsp_run(config, token)
|
||||
await copilotLspRun(config, token)
|
||||
} else if (type_ === 'kcl') {
|
||||
await kcl_lsp_run(config)
|
||||
await kclLspRun(config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1446,6 +1446,9 @@ export class EngineCommandManager {
|
||||
if (this.engineConnection === undefined) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
if (!this.engineConnection?.isReady()) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
if (id === undefined) {
|
||||
throw new Error('id is undefined')
|
||||
}
|
||||
|
@ -7,6 +7,9 @@ import init, {
|
||||
is_points_ccw,
|
||||
get_tangential_arc_to_info,
|
||||
program_memory_init,
|
||||
ServerConfig,
|
||||
copilot_lsp_run,
|
||||
kcl_lsp_run,
|
||||
} from '../wasm-lib/pkg/wasm_lib'
|
||||
import { KCLError } from './errors'
|
||||
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
||||
@ -279,3 +282,27 @@ export function programMemoryInit(): ProgramMemory {
|
||||
throw kclError
|
||||
}
|
||||
}
|
||||
|
||||
export async function copilotLspRun(config: ServerConfig, token: string) {
|
||||
try {
|
||||
console.log('starting copilot lsp')
|
||||
await copilot_lsp_run(config, token)
|
||||
} catch (e: any) {
|
||||
console.log('copilot lsp failed', e)
|
||||
// We make it restart recursively so that if it ever dies after like
|
||||
// 8 hours or something it will come back to life.
|
||||
await copilotLspRun(config, token)
|
||||
}
|
||||
}
|
||||
|
||||
export async function kclLspRun(config: ServerConfig) {
|
||||
try {
|
||||
console.log('start kcl lsp')
|
||||
await kcl_lsp_run(config)
|
||||
} catch (e: any) {
|
||||
console.log('kcl lsp failed', e)
|
||||
// We make it restart recursively so that if it ever dies after like
|
||||
// 8 hours or something it will come back to life.
|
||||
await kclLspRun(config)
|
||||
}
|
||||
}
|
||||
|
11
src/wasm-lib/Cargo.lock
generated
11
src/wasm-lib/Cargo.lock
generated
@ -651,6 +651,16 @@ dependencies = [
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console_error_panic_hook"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
@ -4869,6 +4879,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bson",
|
||||
"console_error_panic_hook",
|
||||
"futures",
|
||||
"gloo-utils",
|
||||
"image",
|
||||
|
@ -30,6 +30,7 @@ twenty-twenty = "0.7"
|
||||
uuid = { version = "1.7.0", features = ["v4", "js", "serde"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.30"
|
||||
js-sys = "0.3.69"
|
||||
tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] }
|
||||
|
@ -5,7 +5,7 @@ use tower_lsp::lsp_types::{
|
||||
CreateFilesParams, DeleteFilesParams, DidChangeConfigurationParams, DidChangeTextDocumentParams,
|
||||
DidChangeWatchedFilesParams, DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams,
|
||||
DidOpenTextDocumentParams, DidSaveTextDocumentParams, InitializedParams, MessageType, RenameFilesParams,
|
||||
TextDocumentItem,
|
||||
TextDocumentItem, WorkspaceFolder,
|
||||
};
|
||||
|
||||
/// A trait for the backend of the language server.
|
||||
@ -15,12 +15,21 @@ pub trait Backend {
|
||||
|
||||
fn fs(&self) -> crate::fs::FileManager;
|
||||
|
||||
fn workspace_folders(&self) -> Vec<WorkspaceFolder>;
|
||||
|
||||
fn add_workspace_folders(&self, folders: Vec<WorkspaceFolder>);
|
||||
|
||||
fn remove_workspace_folders(&self, folders: Vec<WorkspaceFolder>);
|
||||
|
||||
/// Get the current code map.
|
||||
fn current_code_map(&self) -> DashMap<String, String>;
|
||||
|
||||
/// Insert a new code map.
|
||||
fn insert_current_code_map(&self, uri: String, text: String);
|
||||
|
||||
/// Clear the current code state.
|
||||
fn clear_code_state(&self);
|
||||
|
||||
/// On change event.
|
||||
async fn on_change(&self, params: TextDocumentItem);
|
||||
|
||||
@ -43,9 +52,11 @@ pub trait Backend {
|
||||
}
|
||||
|
||||
async fn do_did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) {
|
||||
self.client()
|
||||
.log_message(MessageType::INFO, format!("workspace folders changed: {:?}", params))
|
||||
.await;
|
||||
self.add_workspace_folders(params.event.added);
|
||||
self.remove_workspace_folders(params.event.removed);
|
||||
// Remove the code from the current code map.
|
||||
// We do this since it means the user is changing projects so let's refresh the state.
|
||||
self.clear_code_state();
|
||||
}
|
||||
|
||||
async fn do_did_change_configuration(&self, params: DidChangeConfigurationParams) {
|
||||
@ -117,5 +128,14 @@ pub trait Backend {
|
||||
self.client()
|
||||
.log_message(MessageType::INFO, format!("document closed: {:?}", params))
|
||||
.await;
|
||||
self.client()
|
||||
.log_message(MessageType::INFO, format!("uri: {:?}", params.text_document.uri))
|
||||
.await;
|
||||
// Get the workspace folders.
|
||||
// The key of the workspace folder is the project name.
|
||||
let workspace_folders = self.workspace_folders();
|
||||
self.client()
|
||||
.log_message(MessageType::INFO, format!("workspace: {:?}", workspace_folders))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,8 @@ use tower_lsp::{
|
||||
DidChangeWatchedFilesParams, DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams,
|
||||
DidOpenTextDocumentParams, DidSaveTextDocumentParams, InitializeParams, InitializeResult, InitializedParams,
|
||||
MessageType, OneOf, RenameFilesParams, ServerCapabilities, TextDocumentItem, TextDocumentSyncCapability,
|
||||
TextDocumentSyncKind, TextDocumentSyncOptions, WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities,
|
||||
TextDocumentSyncKind, TextDocumentSyncOptions, WorkspaceFolder, WorkspaceFoldersServerCapabilities,
|
||||
WorkspaceServerCapabilities,
|
||||
},
|
||||
LanguageServer,
|
||||
};
|
||||
@ -44,6 +45,8 @@ pub struct Backend {
|
||||
pub client: tower_lsp::Client,
|
||||
/// The file system client to use.
|
||||
pub fs: crate::fs::FileManager,
|
||||
/// The workspace folders.
|
||||
pub workspace_folders: DashMap<String, WorkspaceFolder>,
|
||||
/// Current code.
|
||||
pub current_code_map: DashMap<String, String>,
|
||||
/// The token is used to authenticate requests to the API server.
|
||||
@ -65,6 +68,22 @@ impl crate::lsp::backend::Backend for Backend {
|
||||
self.fs.clone()
|
||||
}
|
||||
|
||||
fn workspace_folders(&self) -> Vec<WorkspaceFolder> {
|
||||
self.workspace_folders.iter().map(|v| v.value().clone()).collect()
|
||||
}
|
||||
|
||||
fn add_workspace_folders(&self, folders: Vec<WorkspaceFolder>) {
|
||||
for folder in folders {
|
||||
self.workspace_folders.insert(folder.name.to_string(), folder);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_workspace_folders(&self, folders: Vec<WorkspaceFolder>) {
|
||||
for folder in folders {
|
||||
self.workspace_folders.remove(&folder.name);
|
||||
}
|
||||
}
|
||||
|
||||
fn current_code_map(&self) -> DashMap<String, String> {
|
||||
self.current_code_map.clone()
|
||||
}
|
||||
@ -73,6 +92,10 @@ impl crate::lsp::backend::Backend for Backend {
|
||||
self.current_code_map.insert(uri, text);
|
||||
}
|
||||
|
||||
fn clear_code_state(&self) {
|
||||
self.current_code_map.clear();
|
||||
}
|
||||
|
||||
async fn on_change(&self, _params: TextDocumentItem) {
|
||||
// We don't need to do anything here.
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ use tower_lsp::{
|
||||
SemanticTokensServerCapabilities, ServerCapabilities, SignatureHelp, SignatureHelpOptions, SignatureHelpParams,
|
||||
StaticRegistrationOptions, TextDocumentItem, TextDocumentRegistrationOptions, TextDocumentSyncCapability,
|
||||
TextDocumentSyncKind, TextDocumentSyncOptions, TextEdit, WorkDoneProgressOptions, WorkspaceEdit,
|
||||
WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities,
|
||||
WorkspaceFolder, WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities,
|
||||
},
|
||||
Client, LanguageServer,
|
||||
};
|
||||
@ -49,6 +49,8 @@ pub struct Backend {
|
||||
pub client: Client,
|
||||
/// The file system client to use.
|
||||
pub fs: crate::fs::FileManager,
|
||||
/// The workspace folders.
|
||||
pub workspace_folders: DashMap<String, WorkspaceFolder>,
|
||||
/// The stdlib completions for the language.
|
||||
pub stdlib_completions: HashMap<String, CompletionItem>,
|
||||
/// The stdlib signatures for the language.
|
||||
@ -80,6 +82,22 @@ impl crate::lsp::backend::Backend for Backend {
|
||||
self.fs.clone()
|
||||
}
|
||||
|
||||
fn workspace_folders(&self) -> Vec<WorkspaceFolder> {
|
||||
self.workspace_folders.iter().map(|v| v.value().clone()).collect()
|
||||
}
|
||||
|
||||
fn add_workspace_folders(&self, folders: Vec<WorkspaceFolder>) {
|
||||
for folder in folders {
|
||||
self.workspace_folders.insert(folder.name.to_string(), folder);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_workspace_folders(&self, folders: Vec<WorkspaceFolder>) {
|
||||
for folder in folders {
|
||||
self.workspace_folders.remove(&folder.name);
|
||||
}
|
||||
}
|
||||
|
||||
fn current_code_map(&self) -> DashMap<String, String> {
|
||||
self.current_code_map.clone()
|
||||
}
|
||||
@ -88,6 +106,15 @@ impl crate::lsp::backend::Backend for Backend {
|
||||
self.current_code_map.insert(uri, text);
|
||||
}
|
||||
|
||||
fn clear_code_state(&self) {
|
||||
self.current_code_map.clear();
|
||||
self.token_map.clear();
|
||||
self.ast_map.clear();
|
||||
self.diagnostics_map.clear();
|
||||
self.symbols_map.clear();
|
||||
self.semantic_tokens_map.clear();
|
||||
}
|
||||
|
||||
async fn on_change(&self, params: TextDocumentItem) {
|
||||
// We already updated the code map in the shared backend.
|
||||
|
||||
|
@ -19,6 +19,7 @@ pub async fn execute_wasm(
|
||||
engine_manager: kcl_lib::engine::conn_wasm::EngineCommandManager,
|
||||
fs_manager: kcl_lib::fs::wasm::FileSystemManager,
|
||||
) -> Result<JsValue, String> {
|
||||
console_error_panic_hook::set_once();
|
||||
// deserialize the ast from a stringified json
|
||||
|
||||
use kcl_lib::executor::ExecutorContext;
|
||||
@ -54,6 +55,8 @@ pub async fn modify_ast_for_sketch_wasm(
|
||||
plane_type: &str,
|
||||
sketch_id: &str,
|
||||
) -> Result<JsValue, String> {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
// deserialize the ast from a stringified json
|
||||
let mut program: kcl_lib::ast::types::Program = serde_json::from_str(program_str).map_err(|e| e.to_string())?;
|
||||
|
||||
@ -80,6 +83,8 @@ pub async fn modify_ast_for_sketch_wasm(
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn deserialize_files(data: &[u8]) -> Result<JsValue, JsError> {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let ws_resp: kittycad::types::WebSocketResponse = bson::from_slice(data)?;
|
||||
|
||||
if let Some(success) = ws_resp.success {
|
||||
@ -99,12 +104,16 @@ pub fn deserialize_files(data: &[u8]) -> Result<JsValue, JsError> {
|
||||
// test for this function and by extension lexer are done in javascript land src/lang/tokeniser.test.ts
|
||||
#[wasm_bindgen]
|
||||
pub fn lexer_wasm(js: &str) -> Result<JsValue, JsError> {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let tokens = kcl_lib::token::lexer(js);
|
||||
Ok(JsValue::from_serde(&tokens)?)
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn parse_wasm(js: &str) -> Result<JsValue, String> {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let tokens = kcl_lib::token::lexer(js);
|
||||
let parser = kcl_lib::parser::Parser::new(tokens);
|
||||
let program = parser.ast().map_err(String::from)?;
|
||||
@ -117,6 +126,8 @@ pub fn parse_wasm(js: &str) -> Result<JsValue, String> {
|
||||
// test for this function and by extension the recaster are done in javascript land src/lang/recast.test.ts
|
||||
#[wasm_bindgen]
|
||||
pub fn recast_wasm(json_str: &str) -> Result<JsValue, JsError> {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
// deserialize the ast from a stringified json
|
||||
let program: kcl_lib::ast::types::Program = serde_json::from_str(json_str).map_err(JsError::from)?;
|
||||
|
||||
@ -157,6 +168,8 @@ impl ServerConfig {
|
||||
// NOTE: input needs to be an AsyncIterator<Uint8Array, never, void> specifically
|
||||
#[wasm_bindgen]
|
||||
pub async fn kcl_lsp_run(config: ServerConfig) -> Result<(), JsValue> {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let ServerConfig {
|
||||
into_server,
|
||||
from_server,
|
||||
@ -173,6 +186,7 @@ pub async fn kcl_lsp_run(config: ServerConfig) -> Result<(), JsValue> {
|
||||
let (service, socket) = LspService::new(|client| kcl_lib::lsp::kcl::Backend {
|
||||
client,
|
||||
fs: kcl_lib::fs::FileManager::new(fs),
|
||||
workspace_folders: Default::default(),
|
||||
stdlib_completions,
|
||||
stdlib_signatures,
|
||||
token_types,
|
||||
@ -213,6 +227,8 @@ pub async fn kcl_lsp_run(config: ServerConfig) -> Result<(), JsValue> {
|
||||
// NOTE: input needs to be an AsyncIterator<Uint8Array, never, void> specifically
|
||||
#[wasm_bindgen]
|
||||
pub async fn copilot_lsp_run(config: ServerConfig, token: String) -> Result<(), JsValue> {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let ServerConfig {
|
||||
into_server,
|
||||
from_server,
|
||||
@ -222,6 +238,7 @@ pub async fn copilot_lsp_run(config: ServerConfig, token: String) -> Result<(),
|
||||
let (service, socket) = LspService::build(|client| kcl_lib::lsp::copilot::Backend {
|
||||
client,
|
||||
fs: kcl_lib::fs::FileManager::new(fs),
|
||||
workspace_folders: Default::default(),
|
||||
current_code_map: Default::default(),
|
||||
editor_info: Arc::new(RwLock::new(kcl_lib::lsp::copilot::types::CopilotEditorInfo::default())),
|
||||
cache: kcl_lib::lsp::copilot::cache::CopilotCache::new(),
|
||||
@ -258,6 +275,8 @@ pub async fn copilot_lsp_run(config: ServerConfig, token: String) -> Result<(),
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn is_points_ccw(points: &[f64]) -> i32 {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
kcl_lib::std::utils::is_points_ccw_wasm(points)
|
||||
}
|
||||
|
||||
@ -291,11 +310,13 @@ pub fn get_tangential_arc_to_info(
|
||||
tan_previous_point_y: f64,
|
||||
obtuse: bool,
|
||||
) -> TangentialArcInfoOutputWasm {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let result = kcl_lib::std::utils::get_tangential_arc_to_info(kcl_lib::std::utils::TangentialArcInfoInput {
|
||||
arc_start_point: [arc_start_point_x, arc_start_point_y],
|
||||
arc_end_point: [arc_end_point_x, arc_end_point_y],
|
||||
tan_previous_point: [tan_previous_point_x, tan_previous_point_y],
|
||||
obtuse: obtuse,
|
||||
obtuse,
|
||||
});
|
||||
TangentialArcInfoOutputWasm {
|
||||
center_x: result.center[0],
|
||||
@ -312,6 +333,8 @@ pub fn get_tangential_arc_to_info(
|
||||
/// Create the default program memory.
|
||||
#[wasm_bindgen]
|
||||
pub fn program_memory_init() -> Result<JsValue, String> {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let memory = kcl_lib::executor::ProgramMemory::default();
|
||||
|
||||
// The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the
|
||||
|
Reference in New Issue
Block a user