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:
Jess Frazelle
2024-03-11 17:50:31 -07:00
committed by GitHub
parent cd158f8db0
commit db5657a298
17 changed files with 436 additions and 137 deletions

View File

@ -1,4 +1,4 @@
import { useCallback, MouseEventHandler } from 'react' import { useCallback, MouseEventHandler, useEffect } from 'react'
import { DebugPanel } from './components/DebugPanel' import { DebugPanel } from './components/DebugPanel'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { PaneType, useStore } from './useStore' import { PaneType, useStore } from './useStore'
@ -32,11 +32,17 @@ import { engineCommandManager } from './lang/std/engineConnection'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import { isTauri } from 'lib/isTauri' import { isTauri } from 'lib/isTauri'
import { useLspContext } from 'components/LspProvider'
export function App() { export function App() {
const { project, file } = useLoaderData() as IndexLoaderData const { project, file } = useLoaderData() as IndexLoaderData
const navigate = useNavigate() const navigate = useNavigate()
const filePath = useAbsoluteFilePath() const filePath = useAbsoluteFilePath()
const { onProjectOpen } = useLspContext()
useEffect(() => {
onProjectOpen(project || null, file || null)
}, [])
useHotKeyListener() useHotKeyListener()
const { const {

View File

@ -38,6 +38,7 @@ import { sep } from '@tauri-apps/api/path'
import { paths } from 'lib/paths' import { paths } from 'lib/paths'
import { IndexLoaderData, HomeLoaderData } from 'lib/types' import { IndexLoaderData, HomeLoaderData } from 'lib/types'
import { fileSystemManager } from 'lang/std/fileSystemManager' import { fileSystemManager } from 'lang/std/fileSystemManager'
import LspProvider from 'components/LspProvider'
export const BROWSER_FILE_NAME = 'new' export const BROWSER_FILE_NAME = 'new'
@ -52,7 +53,9 @@ const addGlobalContextToElements = (
...route, ...route,
element: ( element: (
<CommandBarProvider> <CommandBarProvider>
<SettingsAuthProvider>{route.element}</SettingsAuthProvider> <SettingsAuthProvider>
<LspProvider>{route.element}</LspProvider>
</SettingsAuthProvider>
</CommandBarProvider> </CommandBarProvider>
), ),
} }

View File

@ -15,11 +15,20 @@ import { FILE_EXT, sortProject } from 'lib/tauriFS'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import { kclManager } from 'lang/KclSingleton' import { kclManager } from 'lang/KclSingleton'
import { useDocumentHasFocus } from 'hooks/useDocumentHasFocus' import { useDocumentHasFocus } from 'hooks/useDocumentHasFocus'
import { useLspContext } from './LspProvider'
function getIndentationCSS(level: number) { function getIndentationCSS(level: number) {
return `calc(1rem * ${level + 1})` 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({ function RenameForm({
fileOrDir, fileOrDir,
setIsRenaming, setIsRenaming,
@ -147,6 +156,7 @@ const FileTreeItem = ({
level?: number level?: number
}) => { }) => {
const { send, context } = useFileContext() const { send, context } = useFileContext()
const { lspClients } = useLspContext()
const navigate = useNavigate() const navigate = useNavigate()
const [isRenaming, setIsRenaming] = useState(false) const [isRenaming, setIsRenaming] = useState(false)
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false) const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
@ -174,6 +184,28 @@ const FileTreeItem = ({
kclManager.code kclManager.code
) )
} else { } 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 // Open kcl files
navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`) navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`)
} }

View 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)
}

View File

@ -12,6 +12,7 @@ import { Logo } from './Logo'
import { APP_NAME } from 'lib/constants' import { APP_NAME } from 'lib/constants'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import { useLspContext } from './LspProvider'
const ProjectSidebarMenu = ({ const ProjectSidebarMenu = ({
project, project,
@ -22,14 +23,24 @@ const ProjectSidebarMenu = ({
project?: IndexLoaderData['project'] project?: IndexLoaderData['project']
file?: IndexLoaderData['file'] file?: IndexLoaderData['file']
}) => { }) => {
const { onProjectClose } = useLspContext()
return ( 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"> <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" /> <Logo className="w-auto h-5 text-chalkboard-120 dark:text-chalkboard-10 group-hover:text-energy-10" />
</Link> </Link>
{renderAsLink ? ( {renderAsLink ? (
<> <>
<Link <Link
onClick={() => {
onProjectClose(file || null, false)
}}
to={paths.HOME} to={paths.HOME}
className="!no-underline" className="!no-underline"
data-testid="project-sidebar-link" data-testid="project-sidebar-link"
@ -57,6 +68,7 @@ function ProjectMenuPopover({
file?: IndexLoaderData['file'] file?: IndexLoaderData['file']
}) { }) {
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const { onProjectClose } = useLspContext()
return ( return (
<Popover className="relative"> <Popover className="relative">
@ -149,8 +161,10 @@ function ProjectMenuPopover({
</ActionButton> </ActionButton>
{isTauri() && ( {isTauri() && (
<ActionButton <ActionButton
Element="link" Element="button"
to={paths.HOME} onClick={() => {
onProjectClose(file || null, true)
}}
icon={{ icon={{
icon: faHome, icon: faHome,
className: 'p-1', className: 'p-1',

View File

@ -4,9 +4,6 @@ import ReactCodeMirror, {
ViewUpdate, ViewUpdate,
keymap, keymap,
} from '@uiw/react-codemirror' } 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 { TEST } from 'env'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
@ -16,8 +13,6 @@ import { useEffect, useMemo, useRef } from 'react'
import { linter, lintGutter } from '@codemirror/lint' import { linter, lintGutter } from '@codemirror/lint'
import { useStore } from 'useStore' import { useStore } from 'useStore'
import { processCodeMirrorRanges } from 'lib/selections' 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 { EditorView, lineHighlightField } from 'editor/highlightextension'
import { roundOff } from 'lib/utils' import { roundOff } from 'lib/utils'
import { kclErrToDiagnostic } from 'lang/errors' import { kclErrToDiagnostic } from 'lang/errors'
@ -26,14 +21,11 @@ import { useModelingContext } from 'hooks/useModelingContext'
import interact from '@replit/codemirror-interact' import interact from '@replit/codemirror-interact'
import { engineCommandManager } from '../lang/std/engineConnection' import { engineCommandManager } from '../lang/std/engineConnection'
import { kclManager, useKclContext } from 'lang/KclSingleton' import { kclManager, useKclContext } from 'lang/KclSingleton'
import { useFileContext } from 'hooks/useFileContext'
import { ModelingMachineEvent } from 'machines/modelingMachine' import { ModelingMachineEvent } from 'machines/modelingMachine'
import { sceneInfra } from 'clientSideScene/sceneInfra' 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 { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { useLspContext } from './LspProvider'
export const editorShortcutMeta = { export const editorShortcutMeta = {
formatCode: { 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 = ({ export const TextEditor = ({
theme, theme,
}: { }: {
theme: Themes.Light | Themes.Dark theme: Themes.Light | Themes.Dark
}) => { }) => {
const { const { editorView, setEditorView, isShiftDown } = useStore((s) => ({
editorView,
isKclLspServerReady,
isCopilotLspServerReady,
setEditorView,
setIsKclLspServerReady,
setIsCopilotLspServerReady,
isShiftDown,
} = useStore((s) => ({
editorView: s.editorView, editorView: s.editorView,
isKclLspServerReady: s.isKclLspServerReady,
isCopilotLspServerReady: s.isCopilotLspServerReady,
setEditorView: s.setEditorView, setEditorView: s.setEditorView,
setIsKclLspServerReady: s.setIsKclLspServerReady,
setIsCopilotLspServerReady: s.setIsCopilotLspServerReady,
isShiftDown: s.isShiftDown, isShiftDown: s.isShiftDown,
})) }))
const { code, errors } = useKclContext() const { code, errors } = useKclContext()
const lastEvent = useRef({ event: '', time: Date.now() }) const lastEvent = useRef({ event: '', time: Date.now() })
const { overallState } = useNetworkStatus() const { overallState } = useNetworkStatus()
const isNetworkOkay = overallState === NetworkHealthState.Ok const isNetworkOkay = overallState === NetworkHealthState.Ok
const { copilotLSP, kclLSP } = useLspContext()
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined') return if (typeof window === 'undefined') return
@ -108,92 +80,12 @@ export const TextEditor = ({
state, state,
} = useModelingContext() } = useModelingContext()
const { settings, auth } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const textWrapping = settings.context?.textWrapping ?? 'On' const textWrapping = settings.context?.textWrapping ?? 'On'
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const {
context: { project },
} = useFileContext()
const { enable: convertEnabled, handleClick: convertCallback } = const { enable: convertEnabled, handleClick: convertCallback } =
useConvertToVariable() 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 = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
const onChange = async (newCode: string) => { const onChange = async (newCode: string) => {
if (isNetworkOkay) kclManager.setCodeAndExecute(newCode) if (isNetworkOkay) kclManager.setCodeAndExecute(newCode)

View File

@ -88,6 +88,8 @@ interface LSPNotifyMap {
initialized: LSP.InitializedParams initialized: LSP.InitializedParams
'textDocument/didChange': LSP.DidChangeTextDocumentParams 'textDocument/didChange': LSP.DidChangeTextDocumentParams
'textDocument/didOpen': LSP.DidOpenTextDocumentParams 'textDocument/didOpen': LSP.DidOpenTextDocumentParams
'textDocument/didClose': LSP.DidCloseTextDocumentParams
'workspace/didChangeWorkspaceFolders': LSP.DidChangeWorkspaceFoldersParams
} }
export interface LanguageServerClientOptions { export interface LanguageServerClientOptions {
@ -149,6 +151,12 @@ export class LanguageServerClient {
textDocumentDidOpen(params: LSP.DidOpenTextDocumentParams) { textDocumentDidOpen(params: LSP.DidOpenTextDocumentParams) {
this.notify('textDocument/didOpen', params) 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) this.updateSemanticTokens(params.textDocument.uri)
} }
@ -157,6 +165,28 @@ export class LanguageServerClient {
this.updateSemanticTokens(params.textDocument.uri) 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) { async updateSemanticTokens(uri: string) {
const serverCapabilities = this.getServerCapabilities() const serverCapabilities = this.getServerCapabilities()
if (!serverCapabilities.semanticTokensProvider) { if (!serverCapabilities.semanticTokensProvider) {

View File

@ -37,9 +37,9 @@ const CompletionItemKindMap = Object.fromEntries(
export class LanguageServerPlugin implements PluginValue { export class LanguageServerPlugin implements PluginValue {
public client: LanguageServerClient public client: LanguageServerClient
private documentUri: string public documentUri: string
private languageId: string public languageId: string
private workspaceFolders: LSP.WorkspaceFolder[] public workspaceFolders: LSP.WorkspaceFolder[]
private documentVersion: number private documentVersion: number
constructor( constructor(

View File

@ -1,11 +1,7 @@
import init, { import { InitOutput, ServerConfig } from 'wasm-lib/pkg/wasm_lib'
copilot_lsp_run,
InitOutput,
kcl_lsp_run,
ServerConfig,
} from 'wasm-lib/pkg/wasm_lib'
import { FromServer, IntoServer } from './codec' import { FromServer, IntoServer } from './codec'
import { fileSystemManager } from 'lang/std/fileSystemManager' import { fileSystemManager } from 'lang/std/fileSystemManager'
import { copilotLspRun, initPromise, kclLspRun } from 'lang/wasm'
export default class Server { export default class Server {
readonly initOutput: InitOutput readonly initOutput: InitOutput
@ -26,7 +22,7 @@ export default class Server {
intoServer: IntoServer, intoServer: IntoServer,
fromServer: FromServer fromServer: FromServer
): Promise<Server> { ): Promise<Server> {
const initOutput = await init() const initOutput = await initPromise
const server = new Server(initOutput, intoServer, fromServer) const server = new Server(initOutput, intoServer, fromServer)
return server return server
} }
@ -38,12 +34,12 @@ export default class Server {
fileSystemManager fileSystemManager
) )
if (type_ === 'copilot') { if (type_ === 'copilot') {
if (!token) { if (!token || token === '') {
throw new Error('auth token is required for copilot') throw new Error('auth token is required for copilot')
} }
await copilot_lsp_run(config, token) await copilotLspRun(config, token)
} else if (type_ === 'kcl') { } else if (type_ === 'kcl') {
await kcl_lsp_run(config) await kclLspRun(config)
} }
} }
} }

View File

@ -1446,6 +1446,9 @@ export class EngineCommandManager {
if (this.engineConnection === undefined) { if (this.engineConnection === undefined) {
return Promise.resolve() return Promise.resolve()
} }
if (!this.engineConnection?.isReady()) {
return Promise.resolve()
}
if (id === undefined) { if (id === undefined) {
throw new Error('id is undefined') throw new Error('id is undefined')
} }

View File

@ -7,6 +7,9 @@ import init, {
is_points_ccw, is_points_ccw,
get_tangential_arc_to_info, get_tangential_arc_to_info,
program_memory_init, program_memory_init,
ServerConfig,
copilot_lsp_run,
kcl_lsp_run,
} from '../wasm-lib/pkg/wasm_lib' } from '../wasm-lib/pkg/wasm_lib'
import { KCLError } from './errors' import { KCLError } from './errors'
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError' import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
@ -279,3 +282,27 @@ export function programMemoryInit(): ProgramMemory {
throw kclError 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)
}
}

View File

@ -651,6 +651,16 @@ dependencies = [
"windows-sys 0.45.0", "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]] [[package]]
name = "const-oid" name = "const-oid"
version = "0.9.6" version = "0.9.6"
@ -4869,6 +4879,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bson", "bson",
"console_error_panic_hook",
"futures", "futures",
"gloo-utils", "gloo-utils",
"image", "image",

View File

@ -30,6 +30,7 @@ twenty-twenty = "0.7"
uuid = { version = "1.7.0", features = ["v4", "js", "serde"] } uuid = { version = "1.7.0", features = ["v4", "js", "serde"] }
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.7"
futures = "0.3.30" futures = "0.3.30"
js-sys = "0.3.69" js-sys = "0.3.69"
tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] } tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] }

View File

@ -5,7 +5,7 @@ use tower_lsp::lsp_types::{
CreateFilesParams, DeleteFilesParams, DidChangeConfigurationParams, DidChangeTextDocumentParams, CreateFilesParams, DeleteFilesParams, DidChangeConfigurationParams, DidChangeTextDocumentParams,
DidChangeWatchedFilesParams, DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams, DidChangeWatchedFilesParams, DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams,
DidOpenTextDocumentParams, DidSaveTextDocumentParams, InitializedParams, MessageType, RenameFilesParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, InitializedParams, MessageType, RenameFilesParams,
TextDocumentItem, TextDocumentItem, WorkspaceFolder,
}; };
/// A trait for the backend of the language server. /// A trait for the backend of the language server.
@ -15,12 +15,21 @@ pub trait Backend {
fn fs(&self) -> crate::fs::FileManager; 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. /// Get the current code map.
fn current_code_map(&self) -> DashMap<String, String>; fn current_code_map(&self) -> DashMap<String, String>;
/// Insert a new code map. /// Insert a new code map.
fn insert_current_code_map(&self, uri: String, text: String); fn insert_current_code_map(&self, uri: String, text: String);
/// Clear the current code state.
fn clear_code_state(&self);
/// On change event. /// On change event.
async fn on_change(&self, params: TextDocumentItem); async fn on_change(&self, params: TextDocumentItem);
@ -43,9 +52,11 @@ pub trait Backend {
} }
async fn do_did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) { async fn do_did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) {
self.client() self.add_workspace_folders(params.event.added);
.log_message(MessageType::INFO, format!("workspace folders changed: {:?}", params)) self.remove_workspace_folders(params.event.removed);
.await; // 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) { async fn do_did_change_configuration(&self, params: DidChangeConfigurationParams) {
@ -117,5 +128,14 @@ pub trait Backend {
self.client() self.client()
.log_message(MessageType::INFO, format!("document closed: {:?}", params)) .log_message(MessageType::INFO, format!("document closed: {:?}", params))
.await; .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;
} }
} }

View File

@ -18,7 +18,8 @@ use tower_lsp::{
DidChangeWatchedFilesParams, DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams, DidChangeWatchedFilesParams, DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams,
DidOpenTextDocumentParams, DidSaveTextDocumentParams, InitializeParams, InitializeResult, InitializedParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, InitializeParams, InitializeResult, InitializedParams,
MessageType, OneOf, RenameFilesParams, ServerCapabilities, TextDocumentItem, TextDocumentSyncCapability, MessageType, OneOf, RenameFilesParams, ServerCapabilities, TextDocumentItem, TextDocumentSyncCapability,
TextDocumentSyncKind, TextDocumentSyncOptions, WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities, TextDocumentSyncKind, TextDocumentSyncOptions, WorkspaceFolder, WorkspaceFoldersServerCapabilities,
WorkspaceServerCapabilities,
}, },
LanguageServer, LanguageServer,
}; };
@ -44,6 +45,8 @@ pub struct Backend {
pub client: tower_lsp::Client, pub client: tower_lsp::Client,
/// The file system client to use. /// The file system client to use.
pub fs: crate::fs::FileManager, pub fs: crate::fs::FileManager,
/// The workspace folders.
pub workspace_folders: DashMap<String, WorkspaceFolder>,
/// Current code. /// Current code.
pub current_code_map: DashMap<String, String>, pub current_code_map: DashMap<String, String>,
/// The token is used to authenticate requests to the API server. /// 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() 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> { fn current_code_map(&self) -> DashMap<String, String> {
self.current_code_map.clone() self.current_code_map.clone()
} }
@ -73,6 +92,10 @@ impl crate::lsp::backend::Backend for Backend {
self.current_code_map.insert(uri, text); self.current_code_map.insert(uri, text);
} }
fn clear_code_state(&self) {
self.current_code_map.clear();
}
async fn on_change(&self, _params: TextDocumentItem) { async fn on_change(&self, _params: TextDocumentItem) {
// We don't need to do anything here. // We don't need to do anything here.
} }

View File

@ -23,7 +23,7 @@ use tower_lsp::{
SemanticTokensServerCapabilities, ServerCapabilities, SignatureHelp, SignatureHelpOptions, SignatureHelpParams, SemanticTokensServerCapabilities, ServerCapabilities, SignatureHelp, SignatureHelpOptions, SignatureHelpParams,
StaticRegistrationOptions, TextDocumentItem, TextDocumentRegistrationOptions, TextDocumentSyncCapability, StaticRegistrationOptions, TextDocumentItem, TextDocumentRegistrationOptions, TextDocumentSyncCapability,
TextDocumentSyncKind, TextDocumentSyncOptions, TextEdit, WorkDoneProgressOptions, WorkspaceEdit, TextDocumentSyncKind, TextDocumentSyncOptions, TextEdit, WorkDoneProgressOptions, WorkspaceEdit,
WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities, WorkspaceFolder, WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities,
}, },
Client, LanguageServer, Client, LanguageServer,
}; };
@ -49,6 +49,8 @@ pub struct Backend {
pub client: Client, pub client: Client,
/// The file system client to use. /// The file system client to use.
pub fs: crate::fs::FileManager, pub fs: crate::fs::FileManager,
/// The workspace folders.
pub workspace_folders: DashMap<String, WorkspaceFolder>,
/// The stdlib completions for the language. /// The stdlib completions for the language.
pub stdlib_completions: HashMap<String, CompletionItem>, pub stdlib_completions: HashMap<String, CompletionItem>,
/// The stdlib signatures for the language. /// The stdlib signatures for the language.
@ -80,6 +82,22 @@ impl crate::lsp::backend::Backend for Backend {
self.fs.clone() 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> { fn current_code_map(&self) -> DashMap<String, String> {
self.current_code_map.clone() self.current_code_map.clone()
} }
@ -88,6 +106,15 @@ impl crate::lsp::backend::Backend for Backend {
self.current_code_map.insert(uri, text); 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) { async fn on_change(&self, params: TextDocumentItem) {
// We already updated the code map in the shared backend. // We already updated the code map in the shared backend.

View File

@ -19,6 +19,7 @@ pub async fn execute_wasm(
engine_manager: kcl_lib::engine::conn_wasm::EngineCommandManager, engine_manager: kcl_lib::engine::conn_wasm::EngineCommandManager,
fs_manager: kcl_lib::fs::wasm::FileSystemManager, fs_manager: kcl_lib::fs::wasm::FileSystemManager,
) -> Result<JsValue, String> { ) -> Result<JsValue, String> {
console_error_panic_hook::set_once();
// deserialize the ast from a stringified json // deserialize the ast from a stringified json
use kcl_lib::executor::ExecutorContext; use kcl_lib::executor::ExecutorContext;
@ -54,6 +55,8 @@ pub async fn modify_ast_for_sketch_wasm(
plane_type: &str, plane_type: &str,
sketch_id: &str, sketch_id: &str,
) -> Result<JsValue, String> { ) -> Result<JsValue, String> {
console_error_panic_hook::set_once();
// deserialize the ast from a stringified json // 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())?; 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] #[wasm_bindgen]
pub fn deserialize_files(data: &[u8]) -> Result<JsValue, JsError> { 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)?; let ws_resp: kittycad::types::WebSocketResponse = bson::from_slice(data)?;
if let Some(success) = ws_resp.success { 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 // test for this function and by extension lexer are done in javascript land src/lang/tokeniser.test.ts
#[wasm_bindgen] #[wasm_bindgen]
pub fn lexer_wasm(js: &str) -> Result<JsValue, JsError> { pub fn lexer_wasm(js: &str) -> Result<JsValue, JsError> {
console_error_panic_hook::set_once();
let tokens = kcl_lib::token::lexer(js); let tokens = kcl_lib::token::lexer(js);
Ok(JsValue::from_serde(&tokens)?) Ok(JsValue::from_serde(&tokens)?)
} }
#[wasm_bindgen] #[wasm_bindgen]
pub fn parse_wasm(js: &str) -> Result<JsValue, String> { pub fn parse_wasm(js: &str) -> Result<JsValue, String> {
console_error_panic_hook::set_once();
let tokens = kcl_lib::token::lexer(js); let tokens = kcl_lib::token::lexer(js);
let parser = kcl_lib::parser::Parser::new(tokens); let parser = kcl_lib::parser::Parser::new(tokens);
let program = parser.ast().map_err(String::from)?; 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 // test for this function and by extension the recaster are done in javascript land src/lang/recast.test.ts
#[wasm_bindgen] #[wasm_bindgen]
pub fn recast_wasm(json_str: &str) -> Result<JsValue, JsError> { pub fn recast_wasm(json_str: &str) -> Result<JsValue, JsError> {
console_error_panic_hook::set_once();
// deserialize the ast from a stringified json // deserialize the ast from a stringified json
let program: kcl_lib::ast::types::Program = serde_json::from_str(json_str).map_err(JsError::from)?; 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 // NOTE: input needs to be an AsyncIterator<Uint8Array, never, void> specifically
#[wasm_bindgen] #[wasm_bindgen]
pub async fn kcl_lsp_run(config: ServerConfig) -> Result<(), JsValue> { pub async fn kcl_lsp_run(config: ServerConfig) -> Result<(), JsValue> {
console_error_panic_hook::set_once();
let ServerConfig { let ServerConfig {
into_server, into_server,
from_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 { let (service, socket) = LspService::new(|client| kcl_lib::lsp::kcl::Backend {
client, client,
fs: kcl_lib::fs::FileManager::new(fs), fs: kcl_lib::fs::FileManager::new(fs),
workspace_folders: Default::default(),
stdlib_completions, stdlib_completions,
stdlib_signatures, stdlib_signatures,
token_types, 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 // NOTE: input needs to be an AsyncIterator<Uint8Array, never, void> specifically
#[wasm_bindgen] #[wasm_bindgen]
pub async fn copilot_lsp_run(config: ServerConfig, token: String) -> Result<(), JsValue> { pub async fn copilot_lsp_run(config: ServerConfig, token: String) -> Result<(), JsValue> {
console_error_panic_hook::set_once();
let ServerConfig { let ServerConfig {
into_server, into_server,
from_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 { let (service, socket) = LspService::build(|client| kcl_lib::lsp::copilot::Backend {
client, client,
fs: kcl_lib::fs::FileManager::new(fs), fs: kcl_lib::fs::FileManager::new(fs),
workspace_folders: Default::default(),
current_code_map: Default::default(), current_code_map: Default::default(),
editor_info: Arc::new(RwLock::new(kcl_lib::lsp::copilot::types::CopilotEditorInfo::default())), editor_info: Arc::new(RwLock::new(kcl_lib::lsp::copilot::types::CopilotEditorInfo::default())),
cache: kcl_lib::lsp::copilot::cache::CopilotCache::new(), 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] #[wasm_bindgen]
pub fn is_points_ccw(points: &[f64]) -> i32 { pub fn is_points_ccw(points: &[f64]) -> i32 {
console_error_panic_hook::set_once();
kcl_lib::std::utils::is_points_ccw_wasm(points) 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, tan_previous_point_y: f64,
obtuse: bool, obtuse: bool,
) -> TangentialArcInfoOutputWasm { ) -> TangentialArcInfoOutputWasm {
console_error_panic_hook::set_once();
let result = kcl_lib::std::utils::get_tangential_arc_to_info(kcl_lib::std::utils::TangentialArcInfoInput { 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_start_point: [arc_start_point_x, arc_start_point_y],
arc_end_point: [arc_end_point_x, arc_end_point_y], arc_end_point: [arc_end_point_x, arc_end_point_y],
tan_previous_point: [tan_previous_point_x, tan_previous_point_y], tan_previous_point: [tan_previous_point_x, tan_previous_point_y],
obtuse: obtuse, obtuse,
}); });
TangentialArcInfoOutputWasm { TangentialArcInfoOutputWasm {
center_x: result.center[0], center_x: result.center[0],
@ -312,6 +333,8 @@ pub fn get_tangential_arc_to_info(
/// Create the default program memory. /// Create the default program memory.
#[wasm_bindgen] #[wasm_bindgen]
pub fn program_memory_init() -> Result<JsValue, String> { pub fn program_memory_init() -> Result<JsValue, String> {
console_error_panic_hook::set_once();
let memory = kcl_lib::executor::ProgramMemory::default(); let memory = kcl_lib::executor::ProgramMemory::default();
// The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the // The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the