Ghost text (#888)
* copilot Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * refactor layout for copilot lsp Signed-off-by: Jess Frazelle <github@jessfraz.com> * start of server Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * more clippy Signed-off-by: Jess Frazelle <github@jessfraz.com> * cleanup code Signed-off-by: Jess Frazelle <github@jessfraz.com> * fix Signed-off-by: Jess Frazelle <github@jessfraz.com> * compile wasm Signed-off-by: Jess Frazelle <github@jessfraz.com> * make work w wasm Signed-off-by: Jess Frazelle <github@jessfraz.com> * clippy Signed-off-by: Jess Frazelle <github@jessfraz.com> * cleanup Signed-off-by: Jess Frazelle <github@jessfraz.com> * cleanup Signed-off-by: Jess Frazelle <github@jessfraz.com> * cleanup unwraps 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> * tests Signed-off-by: Jess Frazelle <github@jessfraz.com> * point to correct things 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> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * cleanup Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * cleanup Signed-off-by: Jess Frazelle <github@jessfraz.com> * updaes 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> * fixes Signed-off-by: Jess Frazelle <github@jessfraz.com> * shared backend features Signed-off-by: Jess Frazelle <github@jessfraz.com> * framework for workspace Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates; Signed-off-by: Jess Frazelle <github@jessfraz.com> * cleanup; 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> * cleanup lints Signed-off-by: Jess Frazelle <github@jessfraz.com> * fmt Signed-off-by: Jess Frazelle <github@jessfraz.com> --------- Signed-off-by: Jess Frazelle <github@jessfraz.com>
This commit is contained in:
@ -1,3 +1,3 @@
|
|||||||
[codespell]
|
[codespell]
|
||||||
ignore-words-list: crate,everytime,inout,co-ordinate,ot
|
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo
|
||||||
skip: **/target,node_modules,build,**/Cargo.lock
|
skip: **/target,node_modules,build,**/Cargo.lock
|
||||||
|
@ -7,8 +7,8 @@ use std::io::Read;
|
|||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use oauth2::TokenResponse;
|
use oauth2::TokenResponse;
|
||||||
use tauri::{InvokeError, Manager};
|
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
use tauri::{InvokeError, Manager};
|
||||||
const DEFAULT_HOST: &str = "https://api.kittycad.io";
|
const DEFAULT_HOST: &str = "https://api.kittycad.io";
|
||||||
|
|
||||||
/// This command returns the a json string parse from a toml file at the path.
|
/// This command returns the a json string parse from a toml file at the path.
|
||||||
@ -158,10 +158,7 @@ fn show_in_folder(path: String) {
|
|||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
Command::new("open")
|
Command::new("open").args(["-R", &path]).spawn().unwrap();
|
||||||
.args(["-R", &path])
|
|
||||||
.spawn()
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ import { engineCommandManager } from '../lang/std/engineConnection'
|
|||||||
import { kclManager, useKclContext } from 'lang/KclSingleton'
|
import { kclManager, useKclContext } from 'lang/KclSingleton'
|
||||||
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
||||||
import { sceneInfra } from 'clientSideScene/sceneInfra'
|
import { sceneInfra } from 'clientSideScene/sceneInfra'
|
||||||
|
import { copilotBundle } from 'editor/copilot'
|
||||||
|
|
||||||
export const editorShortcutMeta = {
|
export const editorShortcutMeta = {
|
||||||
formatCode: {
|
formatCode: {
|
||||||
@ -46,15 +47,19 @@ export const TextEditor = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
editorView,
|
editorView,
|
||||||
isLSPServerReady,
|
isKclLspServerReady,
|
||||||
|
isCopilotLspServerReady,
|
||||||
setEditorView,
|
setEditorView,
|
||||||
setIsLSPServerReady,
|
setIsKclLspServerReady,
|
||||||
|
setIsCopilotLspServerReady,
|
||||||
isShiftDown,
|
isShiftDown,
|
||||||
} = useStore((s) => ({
|
} = useStore((s) => ({
|
||||||
editorView: s.editorView,
|
editorView: s.editorView,
|
||||||
isLSPServerReady: s.isLSPServerReady,
|
isKclLspServerReady: s.isKclLspServerReady,
|
||||||
|
isCopilotLspServerReady: s.isCopilotLspServerReady,
|
||||||
setEditorView: s.setEditorView,
|
setEditorView: s.setEditorView,
|
||||||
setIsLSPServerReady: s.setIsLSPServerReady,
|
setIsKclLspServerReady: s.setIsKclLspServerReady,
|
||||||
|
setIsCopilotLspServerReady: s.setIsCopilotLspServerReady,
|
||||||
isShiftDown: s.isShiftDown,
|
isShiftDown: s.isShiftDown,
|
||||||
}))
|
}))
|
||||||
const { code, errors } = useKclContext()
|
const { code, errors } = useKclContext()
|
||||||
@ -66,7 +71,7 @@ export const TextEditor = ({
|
|||||||
state,
|
state,
|
||||||
} = useModelingContext()
|
} = useModelingContext()
|
||||||
|
|
||||||
const { settings: { context: { textWrapping } = {} } = {} } =
|
const { settings: { context: { textWrapping } = {} } = {}, auth } =
|
||||||
useSettingsAuthContext()
|
useSettingsAuthContext()
|
||||||
const { commandBarSend } = useCommandsContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
const { enable: convertEnabled, handleClick: convertCallback } =
|
const { enable: convertEnabled, handleClick: convertCallback } =
|
||||||
@ -75,20 +80,20 @@ export const TextEditor = ({
|
|||||||
// So this is a bit weird, we need to initialize the lsp server and client.
|
// 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.
|
// But the server happens async so we break this into two parts.
|
||||||
// Below is the client and server promise.
|
// Below is the client and server promise.
|
||||||
const { lspClient } = useMemo(() => {
|
const { lspClient: kclLspClient } = useMemo(() => {
|
||||||
const intoServer: IntoServer = new IntoServer()
|
const intoServer: IntoServer = new IntoServer()
|
||||||
const fromServer: FromServer = FromServer.create()
|
const fromServer: FromServer = FromServer.create()
|
||||||
const client = new Client(fromServer, intoServer)
|
const client = new Client(fromServer, intoServer)
|
||||||
if (!TEST) {
|
if (!TEST) {
|
||||||
Server.initialize(intoServer, fromServer).then((lspServer) => {
|
Server.initialize(intoServer, fromServer).then((lspServer) => {
|
||||||
lspServer.start()
|
lspServer.start('kcl')
|
||||||
setIsLSPServerReady(true)
|
setIsKclLspServerReady(true)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const lspClient = new LanguageServerClient({ client })
|
const lspClient = new LanguageServerClient({ client })
|
||||||
return { lspClient }
|
return { lspClient }
|
||||||
}, [setIsLSPServerReady])
|
}, [setIsKclLspServerReady])
|
||||||
|
|
||||||
// Here we initialize the plugin which will start the client.
|
// 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
|
// When we have multi-file support the name of the file will be a dep of
|
||||||
@ -97,19 +102,57 @@ export const TextEditor = ({
|
|||||||
// We do not want to restart the server, its just wasteful.
|
// We do not want to restart the server, its just wasteful.
|
||||||
const kclLSP = useMemo(() => {
|
const kclLSP = useMemo(() => {
|
||||||
let plugin = null
|
let plugin = null
|
||||||
if (isLSPServerReady && !TEST) {
|
if (isKclLspServerReady && !TEST) {
|
||||||
// Set up the lsp plugin.
|
// Set up the lsp plugin.
|
||||||
const lsp = kclLanguage({
|
const lsp = kclLanguage({
|
||||||
// When we have more than one file, we'll need to change this.
|
// When we have more than one file, we'll need to change this.
|
||||||
documentUri: `file:///we-just-have-one-file-for-now.kcl`,
|
documentUri: `file:///we-just-have-one-file-for-now.kcl`,
|
||||||
workspaceFolders: null,
|
workspaceFolders: null,
|
||||||
client: lspClient,
|
client: kclLspClient,
|
||||||
})
|
})
|
||||||
|
|
||||||
plugin = lsp
|
plugin = lsp
|
||||||
}
|
}
|
||||||
return plugin
|
return plugin
|
||||||
}, [lspClient, isLSPServerReady])
|
}, [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 })
|
||||||
|
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 = copilotBundle({
|
||||||
|
// When we have more than one file, we'll need to change this.
|
||||||
|
documentUri: `file:///we-just-have-one-file-for-now.kcl`,
|
||||||
|
workspaceFolders: null,
|
||||||
|
client: copilotLspClient,
|
||||||
|
allowHTMLContent: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
plugin = lsp
|
||||||
|
}
|
||||||
|
return plugin
|
||||||
|
}, [copilotLspClient, isCopilotLspServerReady])
|
||||||
|
|
||||||
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
||||||
const onChange = (newCode: string) => {
|
const onChange = (newCode: string) => {
|
||||||
@ -184,6 +227,7 @@ export const TextEditor = ({
|
|||||||
] as Extension[]
|
] as Extension[]
|
||||||
|
|
||||||
if (kclLSP) extensions.push(kclLSP)
|
if (kclLSP) extensions.push(kclLSP)
|
||||||
|
if (copilotLSP) extensions.push(copilotLSP)
|
||||||
|
|
||||||
// These extensions have proven to mess with vitest
|
// These extensions have proven to mess with vitest
|
||||||
if (!TEST) {
|
if (!TEST) {
|
||||||
|
503
src/editor/copilot/index.ts
Normal file
503
src/editor/copilot/index.ts
Normal file
@ -0,0 +1,503 @@
|
|||||||
|
/// Thanks to the Cursor folks for their heavy lifting here.
|
||||||
|
import { indentUnit } from '@codemirror/language'
|
||||||
|
import {
|
||||||
|
Decoration,
|
||||||
|
DecorationSet,
|
||||||
|
EditorView,
|
||||||
|
ViewPlugin,
|
||||||
|
ViewUpdate,
|
||||||
|
} from '@codemirror/view'
|
||||||
|
import {
|
||||||
|
Annotation,
|
||||||
|
EditorState,
|
||||||
|
Extension,
|
||||||
|
Facet,
|
||||||
|
Prec,
|
||||||
|
StateEffect,
|
||||||
|
StateField,
|
||||||
|
Transaction,
|
||||||
|
} from '@codemirror/state'
|
||||||
|
import { completionStatus } from '@codemirror/autocomplete'
|
||||||
|
import { docPathFacet, offsetToPos, posToOffset } from 'editor/lsp/util'
|
||||||
|
import { LanguageServerPlugin } from 'editor/lsp/plugin'
|
||||||
|
import { LanguageServerOptions } from 'editor/lsp/plugin'
|
||||||
|
import { LanguageServerClient } from 'editor/lsp'
|
||||||
|
|
||||||
|
// Create Facet for the current docPath
|
||||||
|
export const docPath = Facet.define<string, string>({
|
||||||
|
combine(value: readonly string[]) {
|
||||||
|
return value[value.length - 1]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const relDocPath = Facet.define<string, string>({
|
||||||
|
combine(value: readonly string[]) {
|
||||||
|
return value[value.length - 1]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const ghostMark = Decoration.mark({ class: 'cm-ghostText' })
|
||||||
|
|
||||||
|
interface Suggestion {
|
||||||
|
text: string
|
||||||
|
displayText: string
|
||||||
|
cursorPos: number
|
||||||
|
startPos: number
|
||||||
|
endPos: number
|
||||||
|
endReplacement: number
|
||||||
|
uuid: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Effects to tell StateEffect what to do with GhostText
|
||||||
|
const addSuggestion = StateEffect.define<Suggestion>()
|
||||||
|
const acceptSuggestion = StateEffect.define<null>()
|
||||||
|
const clearSuggestion = StateEffect.define<null>()
|
||||||
|
const typeFirst = StateEffect.define<number>()
|
||||||
|
|
||||||
|
interface CompletionState {
|
||||||
|
ghostText: GhostText | null
|
||||||
|
}
|
||||||
|
interface GhostText {
|
||||||
|
text: string
|
||||||
|
displayText: string
|
||||||
|
displayPos: number
|
||||||
|
startPos: number
|
||||||
|
endGhostText: number
|
||||||
|
endReplacement: number
|
||||||
|
endPos: number
|
||||||
|
decorations: DecorationSet
|
||||||
|
weirdInsert: boolean
|
||||||
|
uuid: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const completionDecoration = StateField.define<CompletionState>({
|
||||||
|
create(_state: EditorState) {
|
||||||
|
return { ghostText: null }
|
||||||
|
},
|
||||||
|
update(state: CompletionState, transaction: Transaction) {
|
||||||
|
for (const effect of transaction.effects) {
|
||||||
|
if (effect.is(addSuggestion)) {
|
||||||
|
// When adding a suggestion, we set th ghostText
|
||||||
|
const {
|
||||||
|
text,
|
||||||
|
displayText,
|
||||||
|
endReplacement,
|
||||||
|
cursorPos,
|
||||||
|
startPos,
|
||||||
|
endPos,
|
||||||
|
uuid,
|
||||||
|
} = effect.value
|
||||||
|
const endGhostText = cursorPos + displayText.length
|
||||||
|
const decorations = Decoration.set([
|
||||||
|
ghostMark.range(cursorPos, endGhostText),
|
||||||
|
])
|
||||||
|
return {
|
||||||
|
ghostText: {
|
||||||
|
text,
|
||||||
|
displayText,
|
||||||
|
startPos,
|
||||||
|
endPos,
|
||||||
|
decorations,
|
||||||
|
displayPos: cursorPos,
|
||||||
|
endReplacement,
|
||||||
|
endGhostText,
|
||||||
|
weirdInsert: false,
|
||||||
|
uuid,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else if (effect.is(acceptSuggestion)) {
|
||||||
|
if (state.ghostText) {
|
||||||
|
return { ghostText: null }
|
||||||
|
}
|
||||||
|
} else if (effect.is(typeFirst)) {
|
||||||
|
const numChars = effect.value
|
||||||
|
if (state.ghostText && !state.ghostText.weirdInsert) {
|
||||||
|
let {
|
||||||
|
text,
|
||||||
|
displayText,
|
||||||
|
displayPos,
|
||||||
|
startPos,
|
||||||
|
endPos,
|
||||||
|
endGhostText,
|
||||||
|
decorations,
|
||||||
|
endReplacement,
|
||||||
|
uuid,
|
||||||
|
} = state.ghostText
|
||||||
|
|
||||||
|
displayPos += numChars
|
||||||
|
|
||||||
|
displayText = displayText.slice(numChars)
|
||||||
|
|
||||||
|
if (startPos === endGhostText) {
|
||||||
|
return { ghostText: null }
|
||||||
|
} else {
|
||||||
|
decorations = Decoration.set([
|
||||||
|
ghostMark.range(displayPos, endGhostText),
|
||||||
|
])
|
||||||
|
return {
|
||||||
|
ghostText: {
|
||||||
|
text,
|
||||||
|
displayText,
|
||||||
|
startPos,
|
||||||
|
endPos,
|
||||||
|
decorations,
|
||||||
|
endGhostText,
|
||||||
|
endReplacement,
|
||||||
|
uuid,
|
||||||
|
displayPos,
|
||||||
|
weirdInsert: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (effect.is(clearSuggestion)) {
|
||||||
|
return { ghostText: null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (transaction.docChanged && state.ghostText) {
|
||||||
|
// if (transaction.
|
||||||
|
// onsole.log({changes: transaction.changes, transaction})
|
||||||
|
// const newGhostText = state.ghostText.decorations.map(transaction.changes)
|
||||||
|
// return {ghostText: {...state.ghostText, decorations: newGhostText}};
|
||||||
|
// }
|
||||||
|
|
||||||
|
return state
|
||||||
|
},
|
||||||
|
provide: (field) =>
|
||||||
|
EditorView.decorations.from(field, (value) =>
|
||||||
|
value.ghostText ? value.ghostText.decorations : Decoration.none
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
const copilotEvent = Annotation.define<null>()
|
||||||
|
|
||||||
|
/****************************************************************************
|
||||||
|
************************* COMMANDS ******************************************
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
const acceptSuggestionCommand = (
|
||||||
|
copilotClient: LanguageServerClient,
|
||||||
|
view: EditorView
|
||||||
|
) => {
|
||||||
|
// We delete the ghost text and insert the suggestion.
|
||||||
|
// We also set the cursor to the end of the suggestion.
|
||||||
|
const ghostText = view.state.field(completionDecoration)!.ghostText
|
||||||
|
if (!ghostText) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const ghostTextStart = ghostText.displayPos
|
||||||
|
const ghostTextEnd = ghostText.endGhostText
|
||||||
|
|
||||||
|
const actualTextStart = ghostText.startPos
|
||||||
|
const actualTextEnd = ghostText.endPos
|
||||||
|
|
||||||
|
const replacementEnd = ghostText.endReplacement
|
||||||
|
|
||||||
|
const suggestion = ghostText.text
|
||||||
|
|
||||||
|
view.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: ghostTextStart,
|
||||||
|
to: ghostTextEnd,
|
||||||
|
insert: '',
|
||||||
|
},
|
||||||
|
// selection: {anchor: actualTextEnd},
|
||||||
|
effects: acceptSuggestion.of(null),
|
||||||
|
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)],
|
||||||
|
})
|
||||||
|
|
||||||
|
const tmpTextEnd = replacementEnd - (ghostTextEnd - ghostTextStart)
|
||||||
|
|
||||||
|
view.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: actualTextStart,
|
||||||
|
to: tmpTextEnd,
|
||||||
|
insert: suggestion,
|
||||||
|
},
|
||||||
|
selection: { anchor: actualTextEnd },
|
||||||
|
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(true)],
|
||||||
|
})
|
||||||
|
|
||||||
|
copilotClient.accept(ghostText.uuid)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
export const rejectSuggestionCommand = (
|
||||||
|
copilotClient: LanguageServerClient,
|
||||||
|
view: EditorView
|
||||||
|
) => {
|
||||||
|
// We delete the suggestion, then carry through with the original keypress
|
||||||
|
const ghostText = view.state.field(completionDecoration)!.ghostText
|
||||||
|
if (!ghostText) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const ghostTextStart = ghostText.displayPos
|
||||||
|
const ghostTextEnd = ghostText.endGhostText
|
||||||
|
|
||||||
|
view.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: ghostTextStart,
|
||||||
|
to: ghostTextEnd,
|
||||||
|
insert: '',
|
||||||
|
},
|
||||||
|
effects: clearSuggestion.of(null),
|
||||||
|
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)],
|
||||||
|
})
|
||||||
|
|
||||||
|
copilotClient.reject()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const sameKeyCommand = (
|
||||||
|
copilotClient: LanguageServerClient,
|
||||||
|
view: EditorView,
|
||||||
|
key: string
|
||||||
|
) => {
|
||||||
|
// When we type a key that is the same as the first letter of the suggestion, we delete the first letter of the suggestion and carry through with the original keypress
|
||||||
|
const ghostText = view.state.field(completionDecoration)!.ghostText
|
||||||
|
if (!ghostText) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const ghostTextStart = ghostText.displayPos
|
||||||
|
const indent = view.state.facet(indentUnit)
|
||||||
|
|
||||||
|
if (key === 'Tab' && ghostText.displayText.startsWith(indent)) {
|
||||||
|
view.dispatch({
|
||||||
|
selection: { anchor: ghostTextStart + indent.length },
|
||||||
|
effects: typeFirst.of(indent.length),
|
||||||
|
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)],
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
} else if (key === 'Tab') {
|
||||||
|
return acceptSuggestionCommand(copilotClient, view)
|
||||||
|
} else if (ghostText.weirdInsert || key !== ghostText.displayText[0]) {
|
||||||
|
return rejectSuggestionCommand(copilotClient, view)
|
||||||
|
} else if (ghostText.displayText.length === 1) {
|
||||||
|
return acceptSuggestionCommand(copilotClient, view)
|
||||||
|
} else {
|
||||||
|
// Use this to delete the first letter of the suggestion
|
||||||
|
view.dispatch({
|
||||||
|
selection: { anchor: ghostTextStart + 1 },
|
||||||
|
effects: typeFirst.of(1),
|
||||||
|
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)],
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const completionPlugin = (copilotClient: LanguageServerClient) =>
|
||||||
|
EditorView.domEventHandlers({
|
||||||
|
keydown(event, view) {
|
||||||
|
if (
|
||||||
|
event.key !== 'Shift' &&
|
||||||
|
event.key !== 'Control' &&
|
||||||
|
event.key !== 'Alt' &&
|
||||||
|
event.key !== 'Meta'
|
||||||
|
) {
|
||||||
|
return sameKeyCommand(copilotClient, view, event.key)
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mousedown(event, view) {
|
||||||
|
return rejectSuggestionCommand(copilotClient, view)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const viewCompletionPlugin = (copilotClient: LanguageServerClient) =>
|
||||||
|
EditorView.updateListener.of((update) => {
|
||||||
|
if (update.focusChanged) {
|
||||||
|
rejectSuggestionCommand(copilotClient, update.view)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// A view plugin that requests completions from the server after a delay
|
||||||
|
const completionRequester = (client: LanguageServerClient) => {
|
||||||
|
let timeout: any = null
|
||||||
|
let lastPos = 0
|
||||||
|
|
||||||
|
const badUpdate = (update: ViewUpdate) => {
|
||||||
|
for (const tr of update.transactions) {
|
||||||
|
if (tr.annotation(copilotEvent) !== undefined) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const containsGhostText = (update: ViewUpdate) => {
|
||||||
|
return update.state.field(completionDecoration).ghostText != null
|
||||||
|
}
|
||||||
|
const autocompleting = (update: ViewUpdate) => {
|
||||||
|
return completionStatus(update.state) === 'active'
|
||||||
|
}
|
||||||
|
const notFocused = (update: ViewUpdate) => {
|
||||||
|
return !update.view.hasFocus
|
||||||
|
}
|
||||||
|
|
||||||
|
return EditorView.updateListener.of((update: ViewUpdate) => {
|
||||||
|
if (
|
||||||
|
update.docChanged &&
|
||||||
|
!update.transactions.some((tr) =>
|
||||||
|
tr.effects.some((e) => e.is(acceptSuggestion) || e.is(clearSuggestion))
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// Cancel the previous timeout
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
badUpdate(update) ||
|
||||||
|
containsGhostText(update) ||
|
||||||
|
autocompleting(update) ||
|
||||||
|
notFocused(update)
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current position and source
|
||||||
|
const state = update.state
|
||||||
|
const pos = state.selection.main.head
|
||||||
|
const source = state.doc.toString()
|
||||||
|
|
||||||
|
const path = state.facet(docPath)
|
||||||
|
const relativePath = state.facet(relDocPath)
|
||||||
|
const languageId = 'kcl'
|
||||||
|
|
||||||
|
// Set a new timeout to request completion
|
||||||
|
timeout = setTimeout(async () => {
|
||||||
|
// Check if the position has changed
|
||||||
|
if (pos === lastPos) {
|
||||||
|
// Request completion from the server
|
||||||
|
try {
|
||||||
|
const completionResult = await client.getCompletion({
|
||||||
|
doc: {
|
||||||
|
source,
|
||||||
|
tabSize: state.facet(EditorState.tabSize),
|
||||||
|
indentSize: 1,
|
||||||
|
insertSpaces: true,
|
||||||
|
path,
|
||||||
|
uri: `file://${path}`,
|
||||||
|
relativePath,
|
||||||
|
languageId,
|
||||||
|
position: offsetToPos(state.doc, pos),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (completionResult.completions.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
text,
|
||||||
|
displayText,
|
||||||
|
range: { start },
|
||||||
|
position,
|
||||||
|
uuid,
|
||||||
|
} = completionResult.completions[0]
|
||||||
|
|
||||||
|
const startPos = posToOffset(state.doc, {
|
||||||
|
line: start.line,
|
||||||
|
character: start.character,
|
||||||
|
})!
|
||||||
|
|
||||||
|
const endGhostPos =
|
||||||
|
posToOffset(state.doc, {
|
||||||
|
line: position.line,
|
||||||
|
character: position.character,
|
||||||
|
})! + displayText.length
|
||||||
|
// EndPos is the position that marks the complete end
|
||||||
|
// of what is to be replaced when we accept a completion
|
||||||
|
// result
|
||||||
|
const endPos = startPos + text.length
|
||||||
|
|
||||||
|
// Check if the position is still the same
|
||||||
|
if (
|
||||||
|
pos === lastPos &&
|
||||||
|
completionStatus(update.view.state) !== 'active' &&
|
||||||
|
update.view.hasFocus
|
||||||
|
) {
|
||||||
|
// Dispatch an effect to add the suggestion
|
||||||
|
// If the completion starts before the end of the line, check the end of the line with the end of the completion
|
||||||
|
const line = update.view.state.doc.lineAt(pos)
|
||||||
|
if (line.to !== pos) {
|
||||||
|
const ending = update.view.state.doc.sliceString(pos, line.to)
|
||||||
|
if (displayText.endsWith(ending)) {
|
||||||
|
displayText = displayText.slice(
|
||||||
|
0,
|
||||||
|
displayText.length - ending.length
|
||||||
|
)
|
||||||
|
} else if (displayText.includes(ending)) {
|
||||||
|
// Remove the ending
|
||||||
|
update.view.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: pos,
|
||||||
|
to: line.to,
|
||||||
|
insert: '',
|
||||||
|
},
|
||||||
|
selection: { anchor: pos },
|
||||||
|
effects: typeFirst.of(ending.length),
|
||||||
|
annotations: [
|
||||||
|
copilotEvent.of(null),
|
||||||
|
Transaction.addToHistory.of(false),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
update.view.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: pos,
|
||||||
|
to: pos,
|
||||||
|
insert: displayText,
|
||||||
|
},
|
||||||
|
effects: [
|
||||||
|
addSuggestion.of({
|
||||||
|
displayText,
|
||||||
|
endReplacement: endGhostPos,
|
||||||
|
text,
|
||||||
|
cursorPos: pos,
|
||||||
|
startPos,
|
||||||
|
endPos,
|
||||||
|
uuid,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
annotations: [
|
||||||
|
copilotEvent.of(null),
|
||||||
|
Transaction.addToHistory.of(false),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('copilot completion failed', error)
|
||||||
|
// Javascript wait for 500ms for some reason is necessary here.
|
||||||
|
// TODO - FIGURE OUT WHY THIS RESOLVES THE BUG
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 150)
|
||||||
|
// Update the last position
|
||||||
|
lastPos = pos
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function copilotServer(options: LanguageServerOptions) {
|
||||||
|
let plugin: LanguageServerPlugin
|
||||||
|
return ViewPlugin.define(
|
||||||
|
(view) =>
|
||||||
|
(plugin = new LanguageServerPlugin(view, options.allowHTMLContent))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const copilotBundle = (options: LanguageServerOptions): Extension => [
|
||||||
|
docPath.of(options.documentUri.split('/').pop()!),
|
||||||
|
docPathFacet.of(options.documentUri.split('/').pop()!),
|
||||||
|
relDocPath.of(options.documentUri.replace('file://', '')),
|
||||||
|
completionDecoration,
|
||||||
|
Prec.highest(completionPlugin(options.client)),
|
||||||
|
Prec.highest(viewCompletionPlugin(options.client)),
|
||||||
|
completionRequester(options.client),
|
||||||
|
copilotServer(options),
|
||||||
|
]
|
@ -3,6 +3,67 @@ import Client from './client'
|
|||||||
import { LanguageServerPlugin } from './plugin'
|
import { LanguageServerPlugin } from './plugin'
|
||||||
import { SemanticToken, deserializeTokens } from './semantic_tokens'
|
import { SemanticToken, deserializeTokens } from './semantic_tokens'
|
||||||
|
|
||||||
|
export interface CopilotGetCompletionsParams {
|
||||||
|
doc: {
|
||||||
|
source: string
|
||||||
|
tabSize: number
|
||||||
|
indentSize: number
|
||||||
|
insertSpaces: boolean
|
||||||
|
path: string
|
||||||
|
uri: string
|
||||||
|
relativePath: string
|
||||||
|
languageId: string
|
||||||
|
position: {
|
||||||
|
line: number
|
||||||
|
character: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CopilotGetCompletionsResult {
|
||||||
|
completions: {
|
||||||
|
text: string
|
||||||
|
position: {
|
||||||
|
line: number
|
||||||
|
character: number
|
||||||
|
}
|
||||||
|
uuid: string
|
||||||
|
range: {
|
||||||
|
start: {
|
||||||
|
line: number
|
||||||
|
character: number
|
||||||
|
}
|
||||||
|
end: {
|
||||||
|
line: number
|
||||||
|
character: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
displayText: string
|
||||||
|
point: {
|
||||||
|
line: number
|
||||||
|
character: number
|
||||||
|
}
|
||||||
|
region: {
|
||||||
|
start: {
|
||||||
|
line: number
|
||||||
|
character: number
|
||||||
|
}
|
||||||
|
end: {
|
||||||
|
line: number
|
||||||
|
character: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CopilotAcceptCompletionParams {
|
||||||
|
uuid: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CopilotRejectCompletionParams {
|
||||||
|
uuids: string[]
|
||||||
|
}
|
||||||
|
|
||||||
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/
|
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/
|
||||||
|
|
||||||
// Client to server then server to client
|
// Client to server then server to client
|
||||||
@ -17,6 +78,9 @@ interface LSPRequestMap {
|
|||||||
LSP.SemanticTokensParams,
|
LSP.SemanticTokensParams,
|
||||||
LSP.SemanticTokens
|
LSP.SemanticTokens
|
||||||
]
|
]
|
||||||
|
getCompletions: [CopilotGetCompletionsParams, CopilotGetCompletionsResult]
|
||||||
|
notifyAccepted: [CopilotAcceptCompletionParams, any]
|
||||||
|
notifyRejected: [CopilotRejectCompletionParams, any]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client to server
|
// Client to server
|
||||||
@ -55,6 +119,7 @@ export class LanguageServerClient {
|
|||||||
|
|
||||||
private isUpdatingSemanticTokens: boolean = false
|
private isUpdatingSemanticTokens: boolean = false
|
||||||
private semanticTokens: SemanticToken[] = []
|
private semanticTokens: SemanticToken[] = []
|
||||||
|
private queuedUids: string[] = []
|
||||||
|
|
||||||
constructor(options: LanguageServerClientOptions) {
|
constructor(options: LanguageServerClientOptions) {
|
||||||
this.plugins = []
|
this.plugins = []
|
||||||
@ -62,6 +127,7 @@ export class LanguageServerClient {
|
|||||||
|
|
||||||
this.ready = false
|
this.ready = false
|
||||||
|
|
||||||
|
this.queuedUids = []
|
||||||
this.initializePromise = this.initialize()
|
this.initializePromise = this.initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,6 +211,33 @@ export class LanguageServerClient {
|
|||||||
return this.client.notify(method, params)
|
return this.client.notify(method, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getCompletion(params: CopilotGetCompletionsParams) {
|
||||||
|
const response = await this.request('getCompletions', params)
|
||||||
|
//
|
||||||
|
this.queuedUids = [...response.completions.map((c) => c.uuid)]
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
async accept(uuid: string) {
|
||||||
|
const badUids = this.queuedUids.filter((u) => u !== uuid)
|
||||||
|
this.queuedUids = []
|
||||||
|
await this.acceptCompletion({ uuid })
|
||||||
|
await this.rejectCompletions({ uuids: badUids })
|
||||||
|
}
|
||||||
|
|
||||||
|
async reject() {
|
||||||
|
const badUids = this.queuedUids
|
||||||
|
this.queuedUids = []
|
||||||
|
return await this.rejectCompletions({ uuids: badUids })
|
||||||
|
}
|
||||||
|
|
||||||
|
async acceptCompletion(params: CopilotAcceptCompletionParams) {
|
||||||
|
return await this.request('notifyAccepted', params)
|
||||||
|
}
|
||||||
|
async rejectCompletions(params: CopilotRejectCompletionParams) {
|
||||||
|
return await this.request('notifyRejected', params)
|
||||||
|
}
|
||||||
|
|
||||||
private processNotification(notification: Notification) {
|
private processNotification(notification: Notification) {
|
||||||
for (const plugin of this.plugins) plugin.processNotification(notification)
|
for (const plugin of this.plugins) plugin.processNotification(notification)
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
NodeSet,
|
NodeSet,
|
||||||
} from '@lezer/common'
|
} from '@lezer/common'
|
||||||
import { LanguageServerClient } from '.'
|
import { LanguageServerClient } from '.'
|
||||||
import { posToOffset } from './plugin'
|
import { posToOffset } from 'editor/lsp/util'
|
||||||
import { SemanticToken } from './semantic_tokens'
|
import { SemanticToken } from './semantic_tokens'
|
||||||
import { DocInput } from '@codemirror/language'
|
import { DocInput } from '@codemirror/language'
|
||||||
import { tags, styleTags } from '@lezer/highlight'
|
import { tags, styleTags } from '@lezer/highlight'
|
||||||
|
@ -22,10 +22,10 @@ import type {
|
|||||||
} from '@codemirror/autocomplete'
|
} from '@codemirror/autocomplete'
|
||||||
import type { PublishDiagnosticsParams } from 'vscode-languageserver-protocol'
|
import type { PublishDiagnosticsParams } from 'vscode-languageserver-protocol'
|
||||||
import type { ViewUpdate, PluginValue } from '@codemirror/view'
|
import type { ViewUpdate, PluginValue } from '@codemirror/view'
|
||||||
import type { Text } from '@codemirror/state'
|
|
||||||
import type * as LSP from 'vscode-languageserver-protocol'
|
import type * as LSP from 'vscode-languageserver-protocol'
|
||||||
import { LanguageServerClient, Notification } from '.'
|
import { LanguageServerClient, Notification } from '.'
|
||||||
import { Marked } from '@ts-stack/markdown'
|
import { Marked } from '@ts-stack/markdown'
|
||||||
|
import { offsetToPos, posToOffset } from 'editor/lsp/util'
|
||||||
|
|
||||||
const changesDelay = 500
|
const changesDelay = 500
|
||||||
|
|
||||||
@ -343,24 +343,6 @@ export function kclPlugin(options: LanguageServerOptions) {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function posToOffset(
|
|
||||||
doc: Text,
|
|
||||||
pos: { line: number; character: number }
|
|
||||||
): number | undefined {
|
|
||||||
if (pos.line >= doc.lines) return
|
|
||||||
const offset = doc.line(pos.line + 1).from + pos.character
|
|
||||||
if (offset > doc.length) return
|
|
||||||
return offset
|
|
||||||
}
|
|
||||||
|
|
||||||
function offsetToPos(doc: Text, offset: number) {
|
|
||||||
const line = doc.lineAt(offset)
|
|
||||||
return {
|
|
||||||
line: line.number - 1,
|
|
||||||
character: offset - line.from,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatContents(
|
function formatContents(
|
||||||
contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[]
|
contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[]
|
||||||
): string {
|
): string {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import init, {
|
import init, {
|
||||||
|
copilot_lsp_run,
|
||||||
InitOutput,
|
InitOutput,
|
||||||
lsp_run,
|
kcl_lsp_run,
|
||||||
ServerConfig,
|
ServerConfig,
|
||||||
} from '../../wasm-lib/pkg/wasm_lib'
|
} from '../../wasm-lib/pkg/wasm_lib'
|
||||||
import { FromServer, IntoServer } from './codec'
|
import { FromServer, IntoServer } from './codec'
|
||||||
@ -29,8 +30,15 @@ export default class Server {
|
|||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(): Promise<void> {
|
async start(type_: 'kcl' | 'copilot', token?: string): Promise<void> {
|
||||||
const config = new ServerConfig(this.#intoServer, this.#fromServer)
|
const config = new ServerConfig(this.#intoServer, this.#fromServer)
|
||||||
await lsp_run(config)
|
if (type_ === 'copilot') {
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('auth token is required for copilot')
|
||||||
|
}
|
||||||
|
await copilot_lsp_run(config, token)
|
||||||
|
} else if (type_ === 'kcl') {
|
||||||
|
await kcl_lsp_run(config)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
23
src/editor/lsp/util.ts
Normal file
23
src/editor/lsp/util.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { Facet, Text } from '@codemirror/state'
|
||||||
|
|
||||||
|
export function posToOffset(
|
||||||
|
doc: Text,
|
||||||
|
pos: { line: number; character: number }
|
||||||
|
): number | undefined {
|
||||||
|
if (pos.line >= doc.lines) return
|
||||||
|
const offset = doc.line(pos.line + 1).from + pos.character
|
||||||
|
if (offset > doc.length) return
|
||||||
|
return offset
|
||||||
|
}
|
||||||
|
|
||||||
|
export function offsetToPos(doc: Text, offset: number) {
|
||||||
|
const line = doc.lineAt(offset)
|
||||||
|
return {
|
||||||
|
line: line.number - 1,
|
||||||
|
character: offset - line.from,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const docPathFacet = Facet.define<string, string>({
|
||||||
|
combine: (values) => values[values.length - 1],
|
||||||
|
})
|
@ -219,3 +219,8 @@ code {
|
|||||||
word-break: normal;
|
word-break: normal;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cm-ghostText,
|
||||||
|
.cm-ghostText * {
|
||||||
|
color: rgb(120, 120, 120, 0.8) !important;
|
||||||
|
}
|
||||||
|
@ -1653,7 +1653,7 @@ describe('parsing errors', () => {
|
|||||||
|
|
||||||
let _theError
|
let _theError
|
||||||
try {
|
try {
|
||||||
const result = expect(parse(code))
|
let _ = expect(parse(code))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_theError = e
|
_theError = e
|
||||||
}
|
}
|
||||||
|
@ -66,8 +66,10 @@ export interface StoreState {
|
|||||||
setMediaStream: (mediaStream: MediaStream) => void
|
setMediaStream: (mediaStream: MediaStream) => void
|
||||||
isStreamReady: boolean
|
isStreamReady: boolean
|
||||||
setIsStreamReady: (isStreamReady: boolean) => void
|
setIsStreamReady: (isStreamReady: boolean) => void
|
||||||
isLSPServerReady: boolean
|
isKclLspServerReady: boolean
|
||||||
setIsLSPServerReady: (isLSPServerReady: boolean) => void
|
isCopilotLspServerReady: boolean
|
||||||
|
setIsKclLspServerReady: (isKclLspServerReady: boolean) => void
|
||||||
|
setIsCopilotLspServerReady: (isCopilotLspServerReady: boolean) => void
|
||||||
buttonDownInStream: number | undefined
|
buttonDownInStream: number | undefined
|
||||||
setButtonDownInStream: (buttonDownInStream: number | undefined) => void
|
setButtonDownInStream: (buttonDownInStream: number | undefined) => void
|
||||||
didDragInStream: boolean
|
didDragInStream: boolean
|
||||||
@ -120,8 +122,12 @@ export const useStore = create<StoreState>()(
|
|||||||
setMediaStream: (mediaStream) => set({ mediaStream }),
|
setMediaStream: (mediaStream) => set({ mediaStream }),
|
||||||
isStreamReady: false,
|
isStreamReady: false,
|
||||||
setIsStreamReady: (isStreamReady) => set({ isStreamReady }),
|
setIsStreamReady: (isStreamReady) => set({ isStreamReady }),
|
||||||
isLSPServerReady: false,
|
isKclLspServerReady: false,
|
||||||
setIsLSPServerReady: (isLSPServerReady) => set({ isLSPServerReady }),
|
isCopilotLspServerReady: false,
|
||||||
|
setIsKclLspServerReady: (isKclLspServerReady) =>
|
||||||
|
set({ isKclLspServerReady }),
|
||||||
|
setIsCopilotLspServerReady: (isCopilotLspServerReady) =>
|
||||||
|
set({ isCopilotLspServerReady }),
|
||||||
buttonDownInStream: undefined,
|
buttonDownInStream: undefined,
|
||||||
setButtonDownInStream: (buttonDownInStream) => {
|
setButtonDownInStream: (buttonDownInStream) => {
|
||||||
set({ buttonDownInStream })
|
set({ buttonDownInStream })
|
||||||
|
29
src/wasm-lib/Cargo.lock
generated
29
src/wasm-lib/Cargo.lock
generated
@ -522,9 +522,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.31"
|
version = "0.4.34"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
|
checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-tzdata",
|
"android-tzdata",
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
@ -532,7 +532,7 @@ dependencies = [
|
|||||||
"num-traits 0.2.17",
|
"num-traits 0.2.17",
|
||||||
"serde",
|
"serde",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-targets 0.48.5",
|
"windows-targets 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1902,6 +1902,7 @@ dependencies = [
|
|||||||
"parse-display 0.9.0",
|
"parse-display 0.9.0",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"ropey",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@ -1931,9 +1932,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kittycad"
|
name = "kittycad"
|
||||||
version = "0.2.50"
|
version = "0.2.53"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "441d8af990a3aab738f985328aa914a9eee5856131c4c6f1fd2bd61ba9d07f98"
|
checksum = "a086e1a1bbddb3b38959c0f0ce6de6b3a3b7566e38e0b7d5fb101e91911beed4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -3130,10 +3131,12 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
"tokio-rustls 0.24.1",
|
"tokio-rustls 0.24.1",
|
||||||
|
"tokio-util",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"url",
|
"url",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
|
"wasm-streams",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"webpki-roots",
|
"webpki-roots",
|
||||||
"winreg",
|
"winreg",
|
||||||
@ -3257,6 +3260,16 @@ dependencies = [
|
|||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ropey"
|
||||||
|
version = "1.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93411e420bcd1a75ddd1dc3caf18c23155eda2c090631a85af21ba19e97093b5"
|
||||||
|
dependencies = [
|
||||||
|
"smallvec",
|
||||||
|
"str_indices",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rtcp"
|
name = "rtcp"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
@ -3863,6 +3876,12 @@ dependencies = [
|
|||||||
"der",
|
"der",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "str_indices"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e9557cb6521e8d009c51a8666f09356f4b817ba9ba0981a305bd86aee47bd35c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
|
@ -58,7 +58,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
kittycad = { version = "0.2.50", default-features = false, features = ["js"] }
|
kittycad = { version = "0.2.53", default-features = false, features = ["js", "requests"] }
|
||||||
kittycad-execution-plan = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
|
kittycad-execution-plan = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
|
||||||
kittycad-execution-plan-traits = "0.1.10"
|
kittycad-execution-plan-traits = "0.1.10"
|
||||||
kittycad-modeling-session = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
|
kittycad-modeling-session = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
|
||||||
|
@ -19,12 +19,15 @@ dashmap = "5.5.3"
|
|||||||
databake = { version = "0.1.7", features = ["derive"] }
|
databake = { version = "0.1.7", features = ["derive"] }
|
||||||
#derive-docs = { version = "0.1.6" }
|
#derive-docs = { version = "0.1.6" }
|
||||||
derive-docs = { path = "../derive-docs" }
|
derive-docs = { path = "../derive-docs" }
|
||||||
|
futures = { version = "0.3.30" }
|
||||||
gltf-json = "1.4.0"
|
gltf-json = "1.4.0"
|
||||||
kittycad = { workspace = true }
|
kittycad = { workspace = true }
|
||||||
kittycad-execution-plan-macros = { workspace = true }
|
kittycad-execution-plan-macros = { workspace = true }
|
||||||
kittycad-execution-plan-traits = { workspace = true }
|
kittycad-execution-plan-traits = { workspace = true }
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
parse-display = "0.9.0"
|
parse-display = "0.9.0"
|
||||||
|
reqwest = { version = "0.11.24", default-features = false, features = ["stream", "rustls-tls"] }
|
||||||
|
ropey = "1.6.1"
|
||||||
schemars = { version = "0.8.16", features = ["impl_json_schema", "url", "uuid1"] }
|
schemars = { version = "0.8.16", features = ["impl_json_schema", "url", "uuid1"] }
|
||||||
serde = { version = "1.0.193", features = ["derive"] }
|
serde = { version = "1.0.193", features = ["derive"] }
|
||||||
serde_json = "1.0.108"
|
serde_json = "1.0.108"
|
||||||
@ -43,8 +46,6 @@ web-sys = { version = "0.3.68", features = ["console"] }
|
|||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
approx = "0.5"
|
approx = "0.5"
|
||||||
bson = { version = "2.9.0", features = ["uuid-1", "chrono"] }
|
bson = { version = "2.9.0", features = ["uuid-1", "chrono"] }
|
||||||
futures = { version = "0.3.30" }
|
|
||||||
reqwest = { version = "0.11.24", default-features = false }
|
|
||||||
tokio = { version = "1.36.0", features = ["full"] }
|
tokio = { version = "1.36.0", features = ["full"] }
|
||||||
tokio-tungstenite = { version = "0.21.0", features = ["rustls-tls-native-roots"] }
|
tokio-tungstenite = { version = "0.21.0", features = ["rustls-tls-native-roots"] }
|
||||||
tower-lsp = { version = "0.20.0", features = ["proposed"] }
|
tower-lsp = { version = "0.20.0", features = ["proposed"] }
|
||||||
|
@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use serde_json::{Map, Value as JValue};
|
use serde_json::{Map, Value as JValue};
|
||||||
use tower_lsp::lsp_types::{CompletionItem, CompletionItemKind, DocumentSymbol, Range as LspRange, SymbolKind};
|
use tower_lsp::lsp_types::{CompletionItem, CompletionItemKind, DocumentSymbol, Range as LspRange, SymbolKind};
|
||||||
|
|
||||||
pub use self::{literal_value::LiteralValue, none::KclNone};
|
pub use crate::ast::types::{literal_value::LiteralValue, none::KclNone};
|
||||||
use crate::{
|
use crate::{
|
||||||
docs::StdLibFn,
|
docs::StdLibFn,
|
||||||
errors::{KclError, KclErrorDetails},
|
errors::{KclError, KclErrorDetails},
|
||||||
|
119
src/wasm-lib/kcl/src/server/backend.rs
Normal file
119
src/wasm-lib/kcl/src/server/backend.rs
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
//! A shared backend trait for lsp servers memory and behavior.
|
||||||
|
|
||||||
|
use dashmap::DashMap;
|
||||||
|
use tower_lsp::lsp_types::{
|
||||||
|
CreateFilesParams, DeleteFilesParams, DidChangeConfigurationParams, DidChangeTextDocumentParams,
|
||||||
|
DidChangeWatchedFilesParams, DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams,
|
||||||
|
DidOpenTextDocumentParams, DidSaveTextDocumentParams, InitializedParams, MessageType, RenameFilesParams,
|
||||||
|
TextDocumentItem,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A trait for the backend of the language server.
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait Backend {
|
||||||
|
fn client(&self) -> tower_lsp::Client;
|
||||||
|
|
||||||
|
/// 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);
|
||||||
|
|
||||||
|
/// On change event.
|
||||||
|
async fn on_change(&self, params: TextDocumentItem);
|
||||||
|
|
||||||
|
async fn update_memory(&self, params: TextDocumentItem) {
|
||||||
|
// Lets update the tokens.
|
||||||
|
self.insert_current_code_map(params.uri.to_string(), params.text.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn do_initialized(&self, params: InitializedParams) {
|
||||||
|
self.client()
|
||||||
|
.log_message(MessageType::INFO, format!("initialized: {:?}", params))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn do_shutdown(&self) -> tower_lsp::jsonrpc::Result<()> {
|
||||||
|
self.client()
|
||||||
|
.log_message(MessageType::INFO, "shutdown".to_string())
|
||||||
|
.await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn do_did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) {
|
||||||
|
self.client()
|
||||||
|
.log_message(MessageType::INFO, format!("workspace folders changed: {:?}", params))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn do_did_change_configuration(&self, params: DidChangeConfigurationParams) {
|
||||||
|
self.client()
|
||||||
|
.log_message(MessageType::INFO, format!("configuration changed: {:?}", params))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn do_did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
|
||||||
|
self.client()
|
||||||
|
.log_message(MessageType::INFO, format!("watched files changed: {:?}", params))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn do_did_create_files(&self, params: CreateFilesParams) {
|
||||||
|
self.client()
|
||||||
|
.log_message(MessageType::INFO, format!("files created: {:?}", params))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn do_did_rename_files(&self, params: RenameFilesParams) {
|
||||||
|
self.client()
|
||||||
|
.log_message(MessageType::INFO, format!("files renamed: {:?}", params))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn do_did_delete_files(&self, params: DeleteFilesParams) {
|
||||||
|
self.client()
|
||||||
|
.log_message(MessageType::INFO, format!("files deleted: {:?}", params))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn do_did_open(&self, params: DidOpenTextDocumentParams) {
|
||||||
|
let new_params = TextDocumentItem {
|
||||||
|
uri: params.text_document.uri,
|
||||||
|
text: params.text_document.text,
|
||||||
|
version: params.text_document.version,
|
||||||
|
language_id: params.text_document.language_id,
|
||||||
|
};
|
||||||
|
self.update_memory(new_params.clone()).await;
|
||||||
|
self.on_change(new_params).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn do_did_change(&self, mut params: DidChangeTextDocumentParams) {
|
||||||
|
let new_params = TextDocumentItem {
|
||||||
|
uri: params.text_document.uri,
|
||||||
|
text: std::mem::take(&mut params.content_changes[0].text),
|
||||||
|
version: params.text_document.version,
|
||||||
|
language_id: Default::default(),
|
||||||
|
};
|
||||||
|
self.update_memory(new_params.clone()).await;
|
||||||
|
self.on_change(new_params).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn do_did_save(&self, params: DidSaveTextDocumentParams) {
|
||||||
|
if let Some(text) = params.text {
|
||||||
|
let new_params = TextDocumentItem {
|
||||||
|
uri: params.text_document.uri,
|
||||||
|
text,
|
||||||
|
version: Default::default(),
|
||||||
|
language_id: Default::default(),
|
||||||
|
};
|
||||||
|
self.update_memory(new_params.clone()).await;
|
||||||
|
self.on_change(new_params).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn do_did_close(&self, params: DidCloseTextDocumentParams) {
|
||||||
|
self.client()
|
||||||
|
.log_message(MessageType::INFO, format!("document closed: {:?}", params))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
111
src/wasm-lib/kcl/src/server/copilot/cache.rs
Normal file
111
src/wasm-lib/kcl/src/server/copilot/cache.rs
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
//! The cache.
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
fmt::Debug,
|
||||||
|
sync::{Mutex, RwLock},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::server::copilot::types::CopilotCompletionResponse;
|
||||||
|
|
||||||
|
// if file changes, keep the cache.
|
||||||
|
// if line number is different for an existing file, clean.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CopilotCache {
|
||||||
|
inner: RwLock<HashMap<String, Mutex<CopilotCompletionResponse>>>,
|
||||||
|
last_line: RwLock<HashMap<String, Mutex<u32>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CopilotCache {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CopilotCache {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
inner: RwLock::new(HashMap::new()),
|
||||||
|
last_line: RwLock::new(HashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_last_line(&self, uri: &String) -> Option<u32> {
|
||||||
|
let Ok(inner) = self.last_line.read() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let last_line = inner.get(uri);
|
||||||
|
match last_line {
|
||||||
|
Some(last_line) => {
|
||||||
|
let Ok(last_line) = last_line.lock() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(*last_line)
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_cached_response(&self, uri: &String, _lnum: u32) -> Option<CopilotCompletionResponse> {
|
||||||
|
let Ok(inner) = self.inner.read() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let cache = inner.get(uri);
|
||||||
|
match cache {
|
||||||
|
Some(completion_response) => {
|
||||||
|
let Ok(completion_response) = completion_response.lock() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(completion_response.clone())
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_file_cache(&self, uri: &str, completion_response: CopilotCompletionResponse) {
|
||||||
|
let Ok(mut inner) = self.inner.write() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
inner.insert(uri.to_string(), Mutex::new(completion_response));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_last_line(&self, uri: &str, last_line: u32) {
|
||||||
|
let Ok(mut inner) = self.last_line.write() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
inner.insert(uri.to_string(), Mutex::new(last_line));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_cached_result(&self, uri: &String, last_line: u32) -> Option<CopilotCompletionResponse> {
|
||||||
|
let Some(cached_line) = self.get_last_line(uri) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
if last_line != cached_line {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
self.get_cached_response(uri, last_line)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_cached_result(
|
||||||
|
&self,
|
||||||
|
uri: &String,
|
||||||
|
lnum: &u32,
|
||||||
|
completion_response: &CopilotCompletionResponse,
|
||||||
|
) -> Option<CopilotCompletionResponse> {
|
||||||
|
self.set_file_cache(uri, completion_response.clone());
|
||||||
|
self.set_last_line(uri, *lnum);
|
||||||
|
let Ok(inner) = self.inner.write() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let cache = inner.get(uri);
|
||||||
|
match cache {
|
||||||
|
Some(completion_response) => {
|
||||||
|
let Ok(completion_response) = completion_response.lock() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(completion_response.clone())
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
259
src/wasm-lib/kcl/src/server/copilot/mod.rs
Normal file
259
src/wasm-lib/kcl/src/server/copilot/mod.rs
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
//! The copilot lsp server for ghost text.
|
||||||
|
|
||||||
|
pub mod cache;
|
||||||
|
pub mod types;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
fmt::Debug,
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
};
|
||||||
|
|
||||||
|
use dashmap::DashMap;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tower_lsp::{
|
||||||
|
jsonrpc::{Error, Result},
|
||||||
|
lsp_types::{
|
||||||
|
CreateFilesParams, DeleteFilesParams, DidChangeConfigurationParams, DidChangeTextDocumentParams,
|
||||||
|
DidChangeWatchedFilesParams, DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams,
|
||||||
|
DidOpenTextDocumentParams, DidSaveTextDocumentParams, InitializeParams, InitializeResult, InitializedParams,
|
||||||
|
MessageType, OneOf, RenameFilesParams, ServerCapabilities, TextDocumentItem, TextDocumentSyncCapability,
|
||||||
|
TextDocumentSyncKind, TextDocumentSyncOptions, WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities,
|
||||||
|
},
|
||||||
|
LanguageServer,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::server::{
|
||||||
|
backend::Backend as _,
|
||||||
|
copilot::types::{CopilotCompletionResponse, CopilotEditorInfo, CopilotLspCompletionParams, DocParams},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
pub struct Success {
|
||||||
|
success: bool,
|
||||||
|
}
|
||||||
|
impl Success {
|
||||||
|
pub fn new(success: bool) -> Self {
|
||||||
|
Self { success }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Backend {
|
||||||
|
/// The client is used to send notifications and requests to the client.
|
||||||
|
pub client: tower_lsp::Client,
|
||||||
|
/// Current code.
|
||||||
|
pub current_code_map: DashMap<String, String>,
|
||||||
|
/// The token is used to authenticate requests to the API server.
|
||||||
|
pub token: String,
|
||||||
|
/// The editor info is used to store information about the editor.
|
||||||
|
pub editor_info: Arc<RwLock<CopilotEditorInfo>>,
|
||||||
|
/// The cache is used to store the results of previous requests.
|
||||||
|
pub cache: cache::CopilotCache,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement the shared backend trait for the language server.
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl crate::server::backend::Backend for Backend {
|
||||||
|
fn client(&self) -> tower_lsp::Client {
|
||||||
|
self.client.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_code_map(&self) -> DashMap<String, String> {
|
||||||
|
self.current_code_map.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_current_code_map(&self, uri: String, text: String) {
|
||||||
|
self.current_code_map.insert(uri, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_change(&self, _params: TextDocumentItem) {
|
||||||
|
// We don't need to do anything here.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Backend {
|
||||||
|
/// Get completions from the kittycad api.
|
||||||
|
pub async fn get_completions(&self, language: String, prompt: String, suffix: String) -> Result<Vec<String>> {
|
||||||
|
let body = kittycad::types::KclCodeCompletionRequest {
|
||||||
|
prompt: Some(prompt.clone()),
|
||||||
|
suffix: Some(suffix.clone()),
|
||||||
|
max_tokens: Some(500),
|
||||||
|
temperature: Some(1.0),
|
||||||
|
top_p: Some(1.0),
|
||||||
|
// We only handle one completion at a time, for now so don't even waste the tokens.
|
||||||
|
n: Some(1),
|
||||||
|
stop: Some(["unset".to_string()].to_vec()),
|
||||||
|
nwo: None,
|
||||||
|
// We haven't implemented streaming yet.
|
||||||
|
stream: None,
|
||||||
|
extra: Some(kittycad::types::KclCodeCompletionParams {
|
||||||
|
language: Some(language.to_string()),
|
||||||
|
next_indent: None,
|
||||||
|
trim_by_indentation: Some(true),
|
||||||
|
prompt_tokens: Some(prompt.len() as u32),
|
||||||
|
suffix_tokens: Some(suffix.len() as u32),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let kc_client = kittycad::Client::new(&self.token);
|
||||||
|
let resp = kc_client
|
||||||
|
.ai()
|
||||||
|
.create_kcl_code_completions(&body)
|
||||||
|
.await
|
||||||
|
.map_err(|err| Error {
|
||||||
|
code: tower_lsp::jsonrpc::ErrorCode::from(69),
|
||||||
|
data: None,
|
||||||
|
message: Cow::from(format!("Failed to get completions from zoo api: {}", err)),
|
||||||
|
})?;
|
||||||
|
Ok(resp.completions)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_editor_info(&self, params: CopilotEditorInfo) -> Result<Success> {
|
||||||
|
self.client.log_message(MessageType::INFO, "setEditorInfo").await;
|
||||||
|
let copy = Arc::clone(&self.editor_info);
|
||||||
|
let mut lock = copy.write().map_err(|err| Error {
|
||||||
|
code: tower_lsp::jsonrpc::ErrorCode::from(69),
|
||||||
|
data: None,
|
||||||
|
message: Cow::from(format!("Failed lock: {}", err)),
|
||||||
|
})?;
|
||||||
|
*lock = params;
|
||||||
|
Ok(Success::new(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_doc_params(&self, params: &CopilotLspCompletionParams) -> Result<DocParams> {
|
||||||
|
let pos = params.doc.position;
|
||||||
|
let uri = params.doc.uri.to_string();
|
||||||
|
let rope = ropey::Rope::from_str(¶ms.doc.source);
|
||||||
|
let offset = crate::server::util::position_to_offset(pos, &rope).unwrap_or_default();
|
||||||
|
|
||||||
|
Ok(DocParams {
|
||||||
|
uri: uri.to_string(),
|
||||||
|
pos,
|
||||||
|
language: params.doc.language_id.to_string(),
|
||||||
|
prefix: crate::server::util::get_text_before(offset, &rope).unwrap_or_default(),
|
||||||
|
suffix: crate::server::util::get_text_after(offset, &rope).unwrap_or_default(),
|
||||||
|
line_before: crate::server::util::get_line_before(pos, &rope).unwrap_or_default(),
|
||||||
|
rope,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_completions_cycling(
|
||||||
|
&self,
|
||||||
|
params: CopilotLspCompletionParams,
|
||||||
|
) -> Result<CopilotCompletionResponse> {
|
||||||
|
let doc_params = self.get_doc_params(¶ms)?;
|
||||||
|
let cached_result = self.cache.get_cached_result(&doc_params.uri, doc_params.pos.line);
|
||||||
|
if let Some(cached_result) = cached_result {
|
||||||
|
return Ok(cached_result);
|
||||||
|
}
|
||||||
|
|
||||||
|
let doc_params = self.get_doc_params(¶ms)?;
|
||||||
|
let line_before = doc_params.line_before.to_string();
|
||||||
|
|
||||||
|
// Let's not call it yet since it's not our model.
|
||||||
|
/*let completion_list = self
|
||||||
|
.get_completions(doc_params.language, doc_params.prefix, doc_params.suffix)
|
||||||
|
.await
|
||||||
|
.map_err(|err| Error {
|
||||||
|
code: tower_lsp::jsonrpc::ErrorCode::from(69),
|
||||||
|
data: None,
|
||||||
|
message: Cow::from(format!("Failed to get completions: {}", err)),
|
||||||
|
})?;*/
|
||||||
|
let completion_list = vec![];
|
||||||
|
|
||||||
|
let response = CopilotCompletionResponse::from_str_vec(completion_list, line_before, doc_params.pos);
|
||||||
|
self.cache
|
||||||
|
.set_cached_result(&doc_params.uri, &doc_params.pos.line, &response);
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn accept_completions(&self, params: Vec<String>) {
|
||||||
|
self.client
|
||||||
|
.log_message(MessageType::INFO, format!("Accepted completions: {:?}", params))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// TODO: send telemetry data back out that we accepted the completions
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn reject_completions(&self, params: Vec<String>) {
|
||||||
|
self.client
|
||||||
|
.log_message(MessageType::INFO, format!("Rejected completions: {:?}", params))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// TODO: send telemetry data back out that we rejected the completions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tower_lsp::async_trait]
|
||||||
|
impl LanguageServer for Backend {
|
||||||
|
async fn initialize(&self, _: InitializeParams) -> Result<InitializeResult> {
|
||||||
|
Ok(InitializeResult {
|
||||||
|
capabilities: ServerCapabilities {
|
||||||
|
text_document_sync: Some(TextDocumentSyncCapability::Options(TextDocumentSyncOptions {
|
||||||
|
open_close: Some(true),
|
||||||
|
change: Some(TextDocumentSyncKind::FULL),
|
||||||
|
..Default::default()
|
||||||
|
})),
|
||||||
|
workspace: Some(WorkspaceServerCapabilities {
|
||||||
|
workspace_folders: Some(WorkspaceFoldersServerCapabilities {
|
||||||
|
supported: Some(true),
|
||||||
|
change_notifications: Some(OneOf::Left(true)),
|
||||||
|
}),
|
||||||
|
file_operations: None,
|
||||||
|
}),
|
||||||
|
..ServerCapabilities::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn initialized(&self, params: InitializedParams) {
|
||||||
|
self.do_initialized(params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn shutdown(&self) -> tower_lsp::jsonrpc::Result<()> {
|
||||||
|
self.do_shutdown().await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) {
|
||||||
|
self.do_did_change_workspace_folders(params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
|
||||||
|
self.do_did_change_configuration(params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
|
||||||
|
self.do_did_change_watched_files(params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn did_create_files(&self, params: CreateFilesParams) {
|
||||||
|
self.do_did_create_files(params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn did_rename_files(&self, params: RenameFilesParams) {
|
||||||
|
self.do_did_rename_files(params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn did_delete_files(&self, params: DeleteFilesParams) {
|
||||||
|
self.do_did_delete_files(params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn did_open(&self, params: DidOpenTextDocumentParams) {
|
||||||
|
self.do_did_open(params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn did_change(&self, params: DidChangeTextDocumentParams) {
|
||||||
|
self.do_did_change(params.clone()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn did_save(&self, params: DidSaveTextDocumentParams) {
|
||||||
|
self.do_did_save(params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn did_close(&self, params: DidCloseTextDocumentParams) {
|
||||||
|
self.do_did_close(params).await
|
||||||
|
}
|
||||||
|
}
|
130
src/wasm-lib/kcl/src/server/copilot/types.rs
Normal file
130
src/wasm-lib/kcl/src/server/copilot/types.rs
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
//! Types we need for communication with the server.
|
||||||
|
|
||||||
|
use ropey::Rope;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tower_lsp::lsp_types::{Position, Range};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CopilotCyclingCompletion {
|
||||||
|
pub display_text: String, // partial text
|
||||||
|
pub text: String, // fulltext
|
||||||
|
pub range: Range, // start char always 0
|
||||||
|
pub position: Position,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Choices {
|
||||||
|
pub text: String,
|
||||||
|
pub index: i16,
|
||||||
|
pub finish_reason: Option<String>,
|
||||||
|
pub logprobs: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct CopilotCompletionResponse {
|
||||||
|
pub completions: Vec<CopilotCyclingCompletion>,
|
||||||
|
pub cancellation_reason: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CopilotCompletionResponse {
|
||||||
|
pub fn from_str_vec(str_vec: Vec<String>, line_before: String, pos: Position) -> Self {
|
||||||
|
let completions = str_vec
|
||||||
|
.iter()
|
||||||
|
.map(|x| CopilotCyclingCompletion::new(x.to_string(), line_before.to_string(), pos))
|
||||||
|
.collect();
|
||||||
|
Self {
|
||||||
|
completions,
|
||||||
|
cancellation_reason: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CopilotCyclingCompletion {
|
||||||
|
pub fn new(text: String, line_before: String, position: Position) -> Self {
|
||||||
|
let display_text = text.clone();
|
||||||
|
let text = format!("{}{}", line_before, text);
|
||||||
|
let end_char = text.find('\n').unwrap_or(text.len()) as u32;
|
||||||
|
Self {
|
||||||
|
display_text, // partial text
|
||||||
|
text, // fulltext
|
||||||
|
range: Range {
|
||||||
|
start: Position {
|
||||||
|
character: 0,
|
||||||
|
line: position.line,
|
||||||
|
},
|
||||||
|
end: Position {
|
||||||
|
character: end_char,
|
||||||
|
line: position.line,
|
||||||
|
},
|
||||||
|
}, // start char always 0
|
||||||
|
position,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct LanguageEntry {
|
||||||
|
language_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct EditorConfiguration {
|
||||||
|
disabled_languages: Vec<LanguageEntry>,
|
||||||
|
enable_auto_completions: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for EditorConfiguration {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
disabled_languages: vec![],
|
||||||
|
enable_auto_completions: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct EditorInfo {
|
||||||
|
name: String,
|
||||||
|
version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CopilotEditorInfo {
|
||||||
|
editor_configuration: EditorConfiguration,
|
||||||
|
editor_info: EditorInfo,
|
||||||
|
editor_plugin_info: EditorInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DocParams {
|
||||||
|
pub rope: Rope,
|
||||||
|
pub uri: String,
|
||||||
|
pub pos: Position,
|
||||||
|
pub language: String,
|
||||||
|
pub line_before: String,
|
||||||
|
pub prefix: String,
|
||||||
|
pub suffix: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CopilotLspCompletionParams {
|
||||||
|
pub doc: CopilotDocParams,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CopilotDocParams {
|
||||||
|
pub indent_size: u32,
|
||||||
|
pub insert_spaces: bool,
|
||||||
|
pub language_id: String,
|
||||||
|
pub path: String,
|
||||||
|
pub position: Position,
|
||||||
|
pub relative_path: String,
|
||||||
|
pub source: String,
|
||||||
|
pub tab_size: u32,
|
||||||
|
pub uri: String,
|
||||||
|
}
|
692
src/wasm-lib/kcl/src/server/lsp/mod.rs
Normal file
692
src/wasm-lib/kcl/src/server/lsp/mod.rs
Normal file
@ -0,0 +1,692 @@
|
|||||||
|
//! Functions for the `kcl` lsp server.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
#[cfg(feature = "cli")]
|
||||||
|
use clap::Parser;
|
||||||
|
use dashmap::DashMap;
|
||||||
|
use tower_lsp::{
|
||||||
|
jsonrpc::Result as RpcResult,
|
||||||
|
lsp_types::{
|
||||||
|
CompletionItem, CompletionItemKind, CompletionOptions, CompletionParams, CompletionResponse, CreateFilesParams,
|
||||||
|
DeleteFilesParams, DiagnosticOptions, DiagnosticServerCapabilities, DidChangeConfigurationParams,
|
||||||
|
DidChangeTextDocumentParams, DidChangeWatchedFilesParams, DidChangeWorkspaceFoldersParams,
|
||||||
|
DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentDiagnosticParams,
|
||||||
|
DocumentDiagnosticReport, DocumentDiagnosticReportResult, DocumentFilter, DocumentFormattingParams,
|
||||||
|
DocumentSymbol, DocumentSymbolParams, DocumentSymbolResponse, Documentation, FullDocumentDiagnosticReport,
|
||||||
|
Hover, HoverContents, HoverParams, HoverProviderCapability, InitializeParams, InitializeResult,
|
||||||
|
InitializedParams, InlayHint, InlayHintParams, InsertTextFormat, MarkupContent, MarkupKind, MessageType, OneOf,
|
||||||
|
ParameterInformation, ParameterLabel, Position, RelatedFullDocumentDiagnosticReport, RenameFilesParams,
|
||||||
|
RenameParams, SemanticToken, SemanticTokenType, SemanticTokens, SemanticTokensFullOptions,
|
||||||
|
SemanticTokensLegend, SemanticTokensOptions, SemanticTokensParams, SemanticTokensRegistrationOptions,
|
||||||
|
SemanticTokensResult, SemanticTokensServerCapabilities, ServerCapabilities, SignatureHelp,
|
||||||
|
SignatureHelpOptions, SignatureHelpParams, SignatureInformation, StaticRegistrationOptions, TextDocumentItem,
|
||||||
|
TextDocumentRegistrationOptions, TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions,
|
||||||
|
TextEdit, WorkDoneProgressOptions, WorkspaceEdit, WorkspaceFoldersServerCapabilities,
|
||||||
|
WorkspaceServerCapabilities,
|
||||||
|
},
|
||||||
|
Client, LanguageServer,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{ast::types::VariableKind, executor::SourceRange, parser::PIPE_OPERATOR, server::backend::Backend as _};
|
||||||
|
|
||||||
|
/// A subcommand for running the server.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
#[cfg_attr(feature = "cli", derive(Parser))]
|
||||||
|
pub struct Server {
|
||||||
|
/// Port that the server should listen
|
||||||
|
#[cfg_attr(feature = "cli", clap(long, default_value = "8080"))]
|
||||||
|
pub socket: i32,
|
||||||
|
|
||||||
|
/// Listen over stdin and stdout instead of a tcp socket.
|
||||||
|
#[cfg_attr(feature = "cli", clap(short, long, default_value = "false"))]
|
||||||
|
pub stdio: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The lsp server backend.
|
||||||
|
pub struct Backend {
|
||||||
|
/// The client for the backend.
|
||||||
|
pub client: Client,
|
||||||
|
/// The stdlib completions for the language.
|
||||||
|
pub stdlib_completions: HashMap<String, CompletionItem>,
|
||||||
|
/// The stdlib signatures for the language.
|
||||||
|
pub stdlib_signatures: HashMap<String, SignatureHelp>,
|
||||||
|
/// The types of tokens the server supports.
|
||||||
|
pub token_types: Vec<SemanticTokenType>,
|
||||||
|
/// Token maps.
|
||||||
|
pub token_map: DashMap<String, Vec<crate::token::Token>>,
|
||||||
|
/// AST maps.
|
||||||
|
pub ast_map: DashMap<String, crate::ast::types::Program>,
|
||||||
|
/// Current code.
|
||||||
|
pub current_code_map: DashMap<String, String>,
|
||||||
|
/// Diagnostics.
|
||||||
|
pub diagnostics_map: DashMap<String, DocumentDiagnosticReport>,
|
||||||
|
/// Symbols map.
|
||||||
|
pub symbols_map: DashMap<String, Vec<DocumentSymbol>>,
|
||||||
|
/// Semantic tokens map.
|
||||||
|
pub semantic_tokens_map: DashMap<String, Vec<SemanticToken>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement the shared backend trait for the language server.
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl crate::server::backend::Backend for Backend {
|
||||||
|
fn client(&self) -> Client {
|
||||||
|
self.client.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_code_map(&self) -> DashMap<String, String> {
|
||||||
|
self.current_code_map.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_current_code_map(&self, uri: String, text: String) {
|
||||||
|
self.current_code_map.insert(uri, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_change(&self, params: TextDocumentItem) {
|
||||||
|
// We already updated the code map in the shared backend.
|
||||||
|
|
||||||
|
// Lets update the tokens.
|
||||||
|
let tokens = crate::token::lexer(¶ms.text);
|
||||||
|
self.token_map.insert(params.uri.to_string(), tokens.clone());
|
||||||
|
|
||||||
|
// Update the semantic tokens map.
|
||||||
|
let mut semantic_tokens = vec![];
|
||||||
|
let mut last_position = Position::new(0, 0);
|
||||||
|
for token in &tokens {
|
||||||
|
let Ok(mut token_type) = SemanticTokenType::try_from(token.token_type) else {
|
||||||
|
// We continue here because not all tokens can be converted this way, we will get
|
||||||
|
// the rest from the ast.
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if token.token_type == crate::token::TokenType::Word && self.stdlib_completions.contains_key(&token.value) {
|
||||||
|
// This is a stdlib function.
|
||||||
|
token_type = SemanticTokenType::FUNCTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
let token_type_index = match self.get_semantic_token_type_index(token_type.clone()) {
|
||||||
|
Some(index) => index,
|
||||||
|
// This is actually bad this should not fail.
|
||||||
|
// TODO: ensure we never get here.
|
||||||
|
None => {
|
||||||
|
self.client
|
||||||
|
.log_message(
|
||||||
|
MessageType::INFO,
|
||||||
|
format!("token type `{:?}` not accounted for", token_type),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let source_range: SourceRange = token.clone().into();
|
||||||
|
let position = source_range.start_to_lsp_position(¶ms.text);
|
||||||
|
|
||||||
|
let semantic_token = SemanticToken {
|
||||||
|
delta_line: position.line - last_position.line,
|
||||||
|
delta_start: if position.line != last_position.line {
|
||||||
|
position.character
|
||||||
|
} else {
|
||||||
|
position.character - last_position.character
|
||||||
|
},
|
||||||
|
length: token.value.len() as u32,
|
||||||
|
token_type: token_type_index as u32,
|
||||||
|
token_modifiers_bitset: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
semantic_tokens.push(semantic_token);
|
||||||
|
|
||||||
|
last_position = position;
|
||||||
|
}
|
||||||
|
self.semantic_tokens_map.insert(params.uri.to_string(), semantic_tokens);
|
||||||
|
|
||||||
|
// Lets update the ast.
|
||||||
|
let parser = crate::parser::Parser::new(tokens);
|
||||||
|
let result = parser.ast();
|
||||||
|
let ast = match result {
|
||||||
|
Ok(ast) => ast,
|
||||||
|
Err(e) => {
|
||||||
|
let diagnostic = e.to_lsp_diagnostic(¶ms.text);
|
||||||
|
// We got errors, update the diagnostics.
|
||||||
|
self.diagnostics_map.insert(
|
||||||
|
params.uri.to_string(),
|
||||||
|
DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
|
||||||
|
related_documents: None,
|
||||||
|
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||||
|
result_id: None,
|
||||||
|
items: vec![diagnostic.clone()],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Publish the diagnostic.
|
||||||
|
// If the client supports it.
|
||||||
|
self.client
|
||||||
|
.publish_diagnostics(params.uri, vec![diagnostic], None)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the symbols map.
|
||||||
|
self.symbols_map
|
||||||
|
.insert(params.uri.to_string(), ast.get_lsp_symbols(¶ms.text));
|
||||||
|
|
||||||
|
self.ast_map.insert(params.uri.to_string(), ast);
|
||||||
|
// Lets update the diagnostics, since we got no errors.
|
||||||
|
self.diagnostics_map.insert(
|
||||||
|
params.uri.to_string(),
|
||||||
|
DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
|
||||||
|
related_documents: None,
|
||||||
|
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||||
|
result_id: None,
|
||||||
|
items: vec![],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Publish the diagnostic, we reset it here so the client knows the code compiles now.
|
||||||
|
// If the client supports it.
|
||||||
|
self.client.publish_diagnostics(params.uri.clone(), vec![], None).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Backend {
|
||||||
|
fn get_semantic_token_type_index(&self, token_type: SemanticTokenType) -> Option<usize> {
|
||||||
|
self.token_types.iter().position(|x| *x == token_type)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn completions_get_variables_from_ast(&self, file_name: &str) -> Vec<CompletionItem> {
|
||||||
|
let mut completions = vec![];
|
||||||
|
|
||||||
|
let ast = match self.ast_map.get(file_name) {
|
||||||
|
Some(ast) => ast,
|
||||||
|
None => return completions,
|
||||||
|
};
|
||||||
|
|
||||||
|
for item in &ast.body {
|
||||||
|
match item {
|
||||||
|
crate::ast::types::BodyItem::ExpressionStatement(_) => continue,
|
||||||
|
crate::ast::types::BodyItem::ReturnStatement(_) => continue,
|
||||||
|
crate::ast::types::BodyItem::VariableDeclaration(variable) => {
|
||||||
|
// We only want to complete variables.
|
||||||
|
for declaration in &variable.declarations {
|
||||||
|
completions.push(CompletionItem {
|
||||||
|
label: declaration.id.name.to_string(),
|
||||||
|
label_details: None,
|
||||||
|
kind: Some(match variable.kind {
|
||||||
|
crate::ast::types::VariableKind::Let => CompletionItemKind::VARIABLE,
|
||||||
|
crate::ast::types::VariableKind::Const => CompletionItemKind::CONSTANT,
|
||||||
|
crate::ast::types::VariableKind::Var => CompletionItemKind::VARIABLE,
|
||||||
|
crate::ast::types::VariableKind::Fn => CompletionItemKind::FUNCTION,
|
||||||
|
}),
|
||||||
|
detail: Some(variable.kind.to_string()),
|
||||||
|
documentation: None,
|
||||||
|
deprecated: None,
|
||||||
|
preselect: None,
|
||||||
|
sort_text: None,
|
||||||
|
filter_text: None,
|
||||||
|
insert_text: None,
|
||||||
|
insert_text_format: None,
|
||||||
|
insert_text_mode: None,
|
||||||
|
text_edit: None,
|
||||||
|
additional_text_edits: None,
|
||||||
|
command: None,
|
||||||
|
commit_characters: None,
|
||||||
|
data: None,
|
||||||
|
tags: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
completions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tower_lsp::async_trait]
|
||||||
|
impl LanguageServer for Backend {
|
||||||
|
async fn initialize(&self, params: InitializeParams) -> RpcResult<InitializeResult> {
|
||||||
|
self.client
|
||||||
|
.log_message(MessageType::INFO, format!("initialize: {:?}", params))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Ok(InitializeResult {
|
||||||
|
capabilities: ServerCapabilities {
|
||||||
|
completion_provider: Some(CompletionOptions {
|
||||||
|
resolve_provider: Some(false),
|
||||||
|
trigger_characters: Some(vec![".".to_string()]),
|
||||||
|
work_done_progress_options: Default::default(),
|
||||||
|
all_commit_characters: None,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions {
|
||||||
|
..Default::default()
|
||||||
|
})),
|
||||||
|
document_formatting_provider: Some(OneOf::Left(true)),
|
||||||
|
document_symbol_provider: Some(OneOf::Left(true)),
|
||||||
|
hover_provider: Some(HoverProviderCapability::Simple(true)),
|
||||||
|
inlay_hint_provider: Some(OneOf::Left(true)),
|
||||||
|
rename_provider: Some(OneOf::Left(true)),
|
||||||
|
semantic_tokens_provider: Some(SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions(
|
||||||
|
SemanticTokensRegistrationOptions {
|
||||||
|
text_document_registration_options: {
|
||||||
|
TextDocumentRegistrationOptions {
|
||||||
|
document_selector: Some(vec![DocumentFilter {
|
||||||
|
language: Some("kcl".to_string()),
|
||||||
|
scheme: Some("file".to_string()),
|
||||||
|
pattern: None,
|
||||||
|
}]),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
semantic_tokens_options: SemanticTokensOptions {
|
||||||
|
work_done_progress_options: WorkDoneProgressOptions::default(),
|
||||||
|
legend: SemanticTokensLegend {
|
||||||
|
token_types: self.token_types.clone(),
|
||||||
|
token_modifiers: vec![],
|
||||||
|
},
|
||||||
|
range: Some(false),
|
||||||
|
full: Some(SemanticTokensFullOptions::Bool(true)),
|
||||||
|
},
|
||||||
|
static_registration_options: StaticRegistrationOptions::default(),
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
signature_help_provider: Some(SignatureHelpOptions {
|
||||||
|
trigger_characters: None,
|
||||||
|
retrigger_characters: None,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
text_document_sync: Some(TextDocumentSyncCapability::Options(TextDocumentSyncOptions {
|
||||||
|
open_close: Some(true),
|
||||||
|
change: Some(TextDocumentSyncKind::FULL),
|
||||||
|
..Default::default()
|
||||||
|
})),
|
||||||
|
workspace: Some(WorkspaceServerCapabilities {
|
||||||
|
workspace_folders: Some(WorkspaceFoldersServerCapabilities {
|
||||||
|
supported: Some(true),
|
||||||
|
change_notifications: Some(OneOf::Left(true)),
|
||||||
|
}),
|
||||||
|
file_operations: None,
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn initialized(&self, params: InitializedParams) {
|
||||||
|
self.do_initialized(params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn shutdown(&self) -> RpcResult<()> {
|
||||||
|
self.do_shutdown().await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) {
|
||||||
|
self.do_did_change_workspace_folders(params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
|
||||||
|
self.do_did_change_configuration(params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
|
||||||
|
self.do_did_change_watched_files(params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn did_create_files(&self, params: CreateFilesParams) {
|
||||||
|
self.do_did_create_files(params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn did_rename_files(&self, params: RenameFilesParams) {
|
||||||
|
self.do_did_rename_files(params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn did_delete_files(&self, params: DeleteFilesParams) {
|
||||||
|
self.do_did_delete_files(params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn did_open(&self, params: DidOpenTextDocumentParams) {
|
||||||
|
self.do_did_open(params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn did_change(&self, params: DidChangeTextDocumentParams) {
|
||||||
|
self.do_did_change(params.clone()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn did_save(&self, params: DidSaveTextDocumentParams) {
|
||||||
|
self.do_did_save(params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn did_close(&self, params: DidCloseTextDocumentParams) {
|
||||||
|
self.do_did_close(params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn hover(&self, params: HoverParams) -> RpcResult<Option<Hover>> {
|
||||||
|
let filename = params.text_document_position_params.text_document.uri.to_string();
|
||||||
|
|
||||||
|
let Some(current_code) = self.current_code_map.get(&filename) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let pos = position_to_char_index(params.text_document_position_params.position, ¤t_code);
|
||||||
|
|
||||||
|
// Let's iterate over the AST and find the node that contains the cursor.
|
||||||
|
let Some(ast) = self.ast_map.get(&filename) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(value) = ast.get_value_for_position(pos) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(hover) = value.get_hover_value_for_position(pos, ¤t_code) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
match hover {
|
||||||
|
crate::ast::types::Hover::Function { name, range } => {
|
||||||
|
// Get the docs for this function.
|
||||||
|
let Some(completion) = self.stdlib_completions.get(&name) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let Some(docs) = &completion.documentation else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let docs = match docs {
|
||||||
|
Documentation::String(docs) => docs,
|
||||||
|
Documentation::MarkupContent(MarkupContent { value, .. }) => value,
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(label_details) = &completion.label_details else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(Hover {
|
||||||
|
contents: HoverContents::Markup(MarkupContent {
|
||||||
|
kind: MarkupKind::Markdown,
|
||||||
|
value: format!(
|
||||||
|
"```{}{}```\n{}",
|
||||||
|
name,
|
||||||
|
label_details.detail.clone().unwrap_or_default(),
|
||||||
|
docs
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
range: Some(range),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
crate::ast::types::Hover::Signature { .. } => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn completion(&self, params: CompletionParams) -> RpcResult<Option<CompletionResponse>> {
|
||||||
|
let mut completions = vec![CompletionItem {
|
||||||
|
label: PIPE_OPERATOR.to_string(),
|
||||||
|
label_details: None,
|
||||||
|
kind: Some(CompletionItemKind::OPERATOR),
|
||||||
|
detail: Some("A pipe operator.".to_string()),
|
||||||
|
documentation: Some(Documentation::MarkupContent(MarkupContent {
|
||||||
|
kind: MarkupKind::Markdown,
|
||||||
|
value: "A pipe operator.".to_string(),
|
||||||
|
})),
|
||||||
|
deprecated: Some(false),
|
||||||
|
preselect: None,
|
||||||
|
sort_text: None,
|
||||||
|
filter_text: None,
|
||||||
|
insert_text: Some("|> ".to_string()),
|
||||||
|
insert_text_format: Some(InsertTextFormat::PLAIN_TEXT),
|
||||||
|
insert_text_mode: None,
|
||||||
|
text_edit: None,
|
||||||
|
additional_text_edits: None,
|
||||||
|
command: None,
|
||||||
|
commit_characters: None,
|
||||||
|
data: None,
|
||||||
|
tags: None,
|
||||||
|
}];
|
||||||
|
|
||||||
|
completions.extend(self.stdlib_completions.values().cloned());
|
||||||
|
|
||||||
|
// Get our variables from our AST to include in our completions.
|
||||||
|
completions.extend(
|
||||||
|
self.completions_get_variables_from_ast(params.text_document_position.text_document.uri.as_ref())
|
||||||
|
.await,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Some(CompletionResponse::Array(completions)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn diagnostic(&self, params: DocumentDiagnosticParams) -> RpcResult<DocumentDiagnosticReportResult> {
|
||||||
|
let filename = params.text_document.uri.to_string();
|
||||||
|
|
||||||
|
// Get the current diagnostics for this file.
|
||||||
|
let Some(diagnostic) = self.diagnostics_map.get(&filename) else {
|
||||||
|
// Send an empty report.
|
||||||
|
return Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
|
||||||
|
RelatedFullDocumentDiagnosticReport {
|
||||||
|
related_documents: None,
|
||||||
|
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||||
|
result_id: None,
|
||||||
|
items: vec![],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(DocumentDiagnosticReportResult::Report(diagnostic.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn signature_help(&self, params: SignatureHelpParams) -> RpcResult<Option<SignatureHelp>> {
|
||||||
|
let filename = params.text_document_position_params.text_document.uri.to_string();
|
||||||
|
|
||||||
|
let Some(current_code) = self.current_code_map.get(&filename) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let pos = position_to_char_index(params.text_document_position_params.position, ¤t_code);
|
||||||
|
|
||||||
|
// Let's iterate over the AST and find the node that contains the cursor.
|
||||||
|
let Some(ast) = self.ast_map.get(&filename) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(value) = ast.get_value_for_position(pos) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(hover) = value.get_hover_value_for_position(pos, ¤t_code) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
match hover {
|
||||||
|
crate::ast::types::Hover::Function { name, range: _ } => {
|
||||||
|
// Get the docs for this function.
|
||||||
|
let Some(signature) = self.stdlib_signatures.get(&name) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(signature.clone()))
|
||||||
|
}
|
||||||
|
crate::ast::types::Hover::Signature {
|
||||||
|
name,
|
||||||
|
parameter_index,
|
||||||
|
range: _,
|
||||||
|
} => {
|
||||||
|
let Some(signature) = self.stdlib_signatures.get(&name) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut signature = signature.clone();
|
||||||
|
|
||||||
|
signature.active_parameter = Some(parameter_index);
|
||||||
|
|
||||||
|
Ok(Some(signature.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn inlay_hint(&self, _params: InlayHintParams) -> RpcResult<Option<Vec<InlayHint>>> {
|
||||||
|
// TODO: do this
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn semantic_tokens_full(&self, params: SemanticTokensParams) -> RpcResult<Option<SemanticTokensResult>> {
|
||||||
|
let filename = params.text_document.uri.to_string();
|
||||||
|
|
||||||
|
let Some(semantic_tokens) = self.semantic_tokens_map.get(&filename) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(SemanticTokensResult::Tokens(SemanticTokens {
|
||||||
|
result_id: None,
|
||||||
|
data: semantic_tokens.clone(),
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn document_symbol(&self, params: DocumentSymbolParams) -> RpcResult<Option<DocumentSymbolResponse>> {
|
||||||
|
let filename = params.text_document.uri.to_string();
|
||||||
|
|
||||||
|
let Some(symbols) = self.symbols_map.get(&filename) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(DocumentSymbolResponse::Nested(symbols.clone())))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn formatting(&self, params: DocumentFormattingParams) -> RpcResult<Option<Vec<TextEdit>>> {
|
||||||
|
let filename = params.text_document.uri.to_string();
|
||||||
|
|
||||||
|
let Some(current_code) = self.current_code_map.get(&filename) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse the ast.
|
||||||
|
// I don't know if we need to do this again since it should be updated in the context.
|
||||||
|
// But I figure better safe than sorry since this will write back out to the file.
|
||||||
|
let tokens = crate::token::lexer(¤t_code);
|
||||||
|
let parser = crate::parser::Parser::new(tokens);
|
||||||
|
let Ok(ast) = parser.ast() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
// Now recast it.
|
||||||
|
let recast = ast.recast(
|
||||||
|
&crate::ast::types::FormatOptions {
|
||||||
|
tab_size: params.options.tab_size as usize,
|
||||||
|
insert_final_newline: params.options.insert_final_newline.unwrap_or(false),
|
||||||
|
use_tabs: !params.options.insert_spaces,
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
let source_range = SourceRange([0, current_code.len() - 1]);
|
||||||
|
let range = source_range.to_lsp_range(¤t_code);
|
||||||
|
Ok(Some(vec![TextEdit {
|
||||||
|
new_text: recast,
|
||||||
|
range,
|
||||||
|
}]))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn rename(&self, params: RenameParams) -> RpcResult<Option<WorkspaceEdit>> {
|
||||||
|
let filename = params.text_document_position.text_document.uri.to_string();
|
||||||
|
|
||||||
|
let Some(current_code) = self.current_code_map.get(&filename) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse the ast.
|
||||||
|
// I don't know if we need to do this again since it should be updated in the context.
|
||||||
|
// But I figure better safe than sorry since this will write back out to the file.
|
||||||
|
let tokens = crate::token::lexer(¤t_code);
|
||||||
|
let parser = crate::parser::Parser::new(tokens);
|
||||||
|
let Ok(mut ast) = parser.ast() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Let's convert the position to a character index.
|
||||||
|
let pos = position_to_char_index(params.text_document_position.position, ¤t_code);
|
||||||
|
// Now let's perform the rename on the ast.
|
||||||
|
ast.rename_symbol(¶ms.new_name, pos);
|
||||||
|
// Now recast it.
|
||||||
|
let recast = ast.recast(&Default::default(), 0);
|
||||||
|
let source_range = SourceRange([0, current_code.len() - 1]);
|
||||||
|
let range = source_range.to_lsp_range(¤t_code);
|
||||||
|
Ok(Some(WorkspaceEdit {
|
||||||
|
changes: Some(HashMap::from([(
|
||||||
|
params.text_document_position.text_document.uri,
|
||||||
|
vec![TextEdit {
|
||||||
|
new_text: recast,
|
||||||
|
range,
|
||||||
|
}],
|
||||||
|
)])),
|
||||||
|
document_changes: None,
|
||||||
|
change_annotations: None,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get completions from our stdlib.
|
||||||
|
pub fn get_completions_from_stdlib(stdlib: &crate::std::StdLib) -> Result<HashMap<String, CompletionItem>> {
|
||||||
|
let mut completions = HashMap::new();
|
||||||
|
|
||||||
|
for internal_fn in stdlib.fns.values() {
|
||||||
|
completions.insert(internal_fn.name(), internal_fn.to_completion_item());
|
||||||
|
}
|
||||||
|
|
||||||
|
let variable_kinds = VariableKind::to_completion_items()?;
|
||||||
|
for variable_kind in variable_kinds {
|
||||||
|
completions.insert(variable_kind.label.clone(), variable_kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(completions)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get signatures from our stdlib.
|
||||||
|
pub fn get_signatures_from_stdlib(stdlib: &crate::std::StdLib) -> Result<HashMap<String, SignatureHelp>> {
|
||||||
|
let mut signatures = HashMap::new();
|
||||||
|
|
||||||
|
for internal_fn in stdlib.fns.values() {
|
||||||
|
signatures.insert(internal_fn.name(), internal_fn.to_signature_help());
|
||||||
|
}
|
||||||
|
|
||||||
|
let show = SignatureHelp {
|
||||||
|
signatures: vec![SignatureInformation {
|
||||||
|
label: "show".to_string(),
|
||||||
|
documentation: Some(Documentation::MarkupContent(MarkupContent {
|
||||||
|
kind: MarkupKind::PlainText,
|
||||||
|
value: "Show a model.".to_string(),
|
||||||
|
})),
|
||||||
|
parameters: Some(vec![ParameterInformation {
|
||||||
|
label: ParameterLabel::Simple("sg: SketchGroup".to_string()),
|
||||||
|
documentation: Some(Documentation::MarkupContent(MarkupContent {
|
||||||
|
kind: MarkupKind::PlainText,
|
||||||
|
value: "A sketch group.".to_string(),
|
||||||
|
})),
|
||||||
|
}]),
|
||||||
|
active_parameter: None,
|
||||||
|
}],
|
||||||
|
active_signature: Some(0),
|
||||||
|
active_parameter: None,
|
||||||
|
};
|
||||||
|
signatures.insert("show".to_string(), show);
|
||||||
|
|
||||||
|
Ok(signatures)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a position to a character index from the start of the file.
|
||||||
|
fn position_to_char_index(position: Position, code: &str) -> usize {
|
||||||
|
// Get the character position from the start of the file.
|
||||||
|
let mut char_position = 0;
|
||||||
|
for (index, line) in code.lines().enumerate() {
|
||||||
|
if index == position.line as usize {
|
||||||
|
char_position += position.character as usize;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
char_position += line.len() + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
char_position
|
||||||
|
}
|
@ -1,672 +1,6 @@
|
|||||||
//! Functions for the `kcl` lsp server.
|
//! The servers that power the text editor.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
mod backend;
|
||||||
|
pub mod copilot;
|
||||||
use anyhow::Result;
|
pub mod lsp;
|
||||||
#[cfg(feature = "cli")]
|
mod util;
|
||||||
use clap::Parser;
|
|
||||||
use dashmap::DashMap;
|
|
||||||
use tower_lsp::{jsonrpc::Result as RpcResult, lsp_types::*, Client, LanguageServer};
|
|
||||||
|
|
||||||
use crate::{ast::types::VariableKind, executor::SourceRange, parser::PIPE_OPERATOR};
|
|
||||||
|
|
||||||
/// A subcommand for running the server.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
#[cfg_attr(feature = "cli", derive(Parser))]
|
|
||||||
pub struct Server {
|
|
||||||
/// Port that the server should listen
|
|
||||||
#[cfg_attr(feature = "cli", clap(long, default_value = "8080"))]
|
|
||||||
pub socket: i32,
|
|
||||||
|
|
||||||
/// Listen over stdin and stdout instead of a tcp socket.
|
|
||||||
#[cfg_attr(feature = "cli", clap(short, long, default_value = "false"))]
|
|
||||||
pub stdio: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The lsp server backend.
|
|
||||||
pub struct Backend {
|
|
||||||
/// The client for the backend.
|
|
||||||
pub client: Client,
|
|
||||||
/// The stdlib completions for the language.
|
|
||||||
pub stdlib_completions: HashMap<String, CompletionItem>,
|
|
||||||
/// The stdlib signatures for the language.
|
|
||||||
pub stdlib_signatures: HashMap<String, SignatureHelp>,
|
|
||||||
/// The types of tokens the server supports.
|
|
||||||
pub token_types: Vec<SemanticTokenType>,
|
|
||||||
/// Token maps.
|
|
||||||
pub token_map: DashMap<String, Vec<crate::token::Token>>,
|
|
||||||
/// AST maps.
|
|
||||||
pub ast_map: DashMap<String, crate::ast::types::Program>,
|
|
||||||
/// Current code.
|
|
||||||
pub current_code_map: DashMap<String, String>,
|
|
||||||
/// Diagnostics.
|
|
||||||
pub diagnostics_map: DashMap<String, DocumentDiagnosticReport>,
|
|
||||||
/// Symbols map.
|
|
||||||
pub symbols_map: DashMap<String, Vec<DocumentSymbol>>,
|
|
||||||
/// Semantic tokens map.
|
|
||||||
pub semantic_tokens_map: DashMap<String, Vec<SemanticToken>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Backend {
|
|
||||||
fn get_semantic_token_type_index(&self, token_type: SemanticTokenType) -> Option<usize> {
|
|
||||||
self.token_types.iter().position(|x| *x == token_type)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn on_change(&self, params: TextDocumentItem) {
|
|
||||||
// Lets update the tokens.
|
|
||||||
self.current_code_map
|
|
||||||
.insert(params.uri.to_string(), params.text.clone());
|
|
||||||
let tokens = crate::token::lexer(¶ms.text);
|
|
||||||
self.token_map.insert(params.uri.to_string(), tokens.clone());
|
|
||||||
|
|
||||||
// Update the semantic tokens map.
|
|
||||||
let mut semantic_tokens = vec![];
|
|
||||||
let mut last_position = Position::new(0, 0);
|
|
||||||
for token in &tokens {
|
|
||||||
let Ok(mut token_type) = SemanticTokenType::try_from(token.token_type) else {
|
|
||||||
// We continue here because not all tokens can be converted this way, we will get
|
|
||||||
// the rest from the ast.
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
if token.token_type == crate::token::TokenType::Word && self.stdlib_completions.contains_key(&token.value) {
|
|
||||||
// This is a stdlib function.
|
|
||||||
token_type = SemanticTokenType::FUNCTION;
|
|
||||||
}
|
|
||||||
|
|
||||||
let token_type_index = match self.get_semantic_token_type_index(token_type.clone()) {
|
|
||||||
Some(index) => index,
|
|
||||||
// This is actually bad this should not fail.
|
|
||||||
// TODO: ensure we never get here.
|
|
||||||
None => {
|
|
||||||
self.client
|
|
||||||
.log_message(
|
|
||||||
MessageType::INFO,
|
|
||||||
format!("token type `{:?}` not accounted for", token_type),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let source_range: SourceRange = token.clone().into();
|
|
||||||
let position = source_range.start_to_lsp_position(¶ms.text);
|
|
||||||
|
|
||||||
let semantic_token = SemanticToken {
|
|
||||||
delta_line: position.line - last_position.line,
|
|
||||||
delta_start: if position.line != last_position.line {
|
|
||||||
position.character
|
|
||||||
} else {
|
|
||||||
position.character - last_position.character
|
|
||||||
},
|
|
||||||
length: token.value.len() as u32,
|
|
||||||
token_type: token_type_index as u32,
|
|
||||||
token_modifiers_bitset: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
semantic_tokens.push(semantic_token);
|
|
||||||
|
|
||||||
last_position = position;
|
|
||||||
}
|
|
||||||
self.semantic_tokens_map.insert(params.uri.to_string(), semantic_tokens);
|
|
||||||
|
|
||||||
// Lets update the ast.
|
|
||||||
let parser = crate::parser::Parser::new(tokens);
|
|
||||||
let result = parser.ast();
|
|
||||||
let ast = match result {
|
|
||||||
Ok(ast) => ast,
|
|
||||||
Err(e) => {
|
|
||||||
let diagnostic = e.to_lsp_diagnostic(¶ms.text);
|
|
||||||
// We got errors, update the diagnostics.
|
|
||||||
self.diagnostics_map.insert(
|
|
||||||
params.uri.to_string(),
|
|
||||||
DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
|
|
||||||
related_documents: None,
|
|
||||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
|
||||||
result_id: None,
|
|
||||||
items: vec![diagnostic.clone()],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Publish the diagnostic.
|
|
||||||
// If the client supports it.
|
|
||||||
self.client
|
|
||||||
.publish_diagnostics(params.uri, vec![diagnostic], None)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update the symbols map.
|
|
||||||
self.symbols_map
|
|
||||||
.insert(params.uri.to_string(), ast.get_lsp_symbols(¶ms.text));
|
|
||||||
|
|
||||||
self.ast_map.insert(params.uri.to_string(), ast);
|
|
||||||
// Lets update the diagnostics, since we got no errors.
|
|
||||||
self.diagnostics_map.insert(
|
|
||||||
params.uri.to_string(),
|
|
||||||
DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
|
|
||||||
related_documents: None,
|
|
||||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
|
||||||
result_id: None,
|
|
||||||
items: vec![],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Publish the diagnostic, we reset it here so the client knows the code compiles now.
|
|
||||||
// If the client supports it.
|
|
||||||
self.client.publish_diagnostics(params.uri.clone(), vec![], None).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn completions_get_variables_from_ast(&self, file_name: &str) -> Vec<CompletionItem> {
|
|
||||||
let mut completions = vec![];
|
|
||||||
|
|
||||||
let ast = match self.ast_map.get(file_name) {
|
|
||||||
Some(ast) => ast,
|
|
||||||
None => return completions,
|
|
||||||
};
|
|
||||||
|
|
||||||
for item in &ast.body {
|
|
||||||
match item {
|
|
||||||
crate::ast::types::BodyItem::ExpressionStatement(_) => continue,
|
|
||||||
crate::ast::types::BodyItem::ReturnStatement(_) => continue,
|
|
||||||
crate::ast::types::BodyItem::VariableDeclaration(variable) => {
|
|
||||||
// We only want to complete variables.
|
|
||||||
for declaration in &variable.declarations {
|
|
||||||
completions.push(CompletionItem {
|
|
||||||
label: declaration.id.name.to_string(),
|
|
||||||
label_details: None,
|
|
||||||
kind: Some(match variable.kind {
|
|
||||||
crate::ast::types::VariableKind::Let => CompletionItemKind::VARIABLE,
|
|
||||||
crate::ast::types::VariableKind::Const => CompletionItemKind::CONSTANT,
|
|
||||||
crate::ast::types::VariableKind::Var => CompletionItemKind::VARIABLE,
|
|
||||||
crate::ast::types::VariableKind::Fn => CompletionItemKind::FUNCTION,
|
|
||||||
}),
|
|
||||||
detail: Some(variable.kind.to_string()),
|
|
||||||
documentation: None,
|
|
||||||
deprecated: None,
|
|
||||||
preselect: None,
|
|
||||||
sort_text: None,
|
|
||||||
filter_text: None,
|
|
||||||
insert_text: None,
|
|
||||||
insert_text_format: None,
|
|
||||||
insert_text_mode: None,
|
|
||||||
text_edit: None,
|
|
||||||
additional_text_edits: None,
|
|
||||||
command: None,
|
|
||||||
commit_characters: None,
|
|
||||||
data: None,
|
|
||||||
tags: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
completions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tower_lsp::async_trait]
|
|
||||||
impl LanguageServer for Backend {
|
|
||||||
async fn initialize(&self, params: InitializeParams) -> RpcResult<InitializeResult> {
|
|
||||||
self.client
|
|
||||||
.log_message(MessageType::INFO, format!("initialize: {:?}", params))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(InitializeResult {
|
|
||||||
capabilities: ServerCapabilities {
|
|
||||||
completion_provider: Some(CompletionOptions {
|
|
||||||
resolve_provider: Some(false),
|
|
||||||
trigger_characters: Some(vec![".".to_string()]),
|
|
||||||
work_done_progress_options: Default::default(),
|
|
||||||
all_commit_characters: None,
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions {
|
|
||||||
..Default::default()
|
|
||||||
})),
|
|
||||||
document_formatting_provider: Some(OneOf::Left(true)),
|
|
||||||
document_symbol_provider: Some(OneOf::Left(true)),
|
|
||||||
hover_provider: Some(HoverProviderCapability::Simple(true)),
|
|
||||||
inlay_hint_provider: Some(OneOf::Left(true)),
|
|
||||||
rename_provider: Some(OneOf::Left(true)),
|
|
||||||
semantic_tokens_provider: Some(SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions(
|
|
||||||
SemanticTokensRegistrationOptions {
|
|
||||||
text_document_registration_options: {
|
|
||||||
TextDocumentRegistrationOptions {
|
|
||||||
document_selector: Some(vec![DocumentFilter {
|
|
||||||
language: Some("kcl".to_string()),
|
|
||||||
scheme: Some("file".to_string()),
|
|
||||||
pattern: None,
|
|
||||||
}]),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
semantic_tokens_options: SemanticTokensOptions {
|
|
||||||
work_done_progress_options: WorkDoneProgressOptions::default(),
|
|
||||||
legend: SemanticTokensLegend {
|
|
||||||
token_types: self.token_types.clone(),
|
|
||||||
token_modifiers: vec![],
|
|
||||||
},
|
|
||||||
range: Some(false),
|
|
||||||
full: Some(SemanticTokensFullOptions::Bool(true)),
|
|
||||||
},
|
|
||||||
static_registration_options: StaticRegistrationOptions::default(),
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
signature_help_provider: Some(SignatureHelpOptions {
|
|
||||||
trigger_characters: None,
|
|
||||||
retrigger_characters: None,
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
text_document_sync: Some(TextDocumentSyncCapability::Options(TextDocumentSyncOptions {
|
|
||||||
open_close: Some(true),
|
|
||||||
change: Some(TextDocumentSyncKind::FULL),
|
|
||||||
..Default::default()
|
|
||||||
})),
|
|
||||||
workspace: Some(WorkspaceServerCapabilities {
|
|
||||||
workspace_folders: Some(WorkspaceFoldersServerCapabilities {
|
|
||||||
supported: Some(true),
|
|
||||||
change_notifications: Some(OneOf::Left(true)),
|
|
||||||
}),
|
|
||||||
file_operations: None,
|
|
||||||
}),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn initialized(&self, params: InitializedParams) {
|
|
||||||
self.client
|
|
||||||
.log_message(MessageType::INFO, format!("initialized: {:?}", params))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn shutdown(&self) -> RpcResult<()> {
|
|
||||||
self.client.log_message(MessageType::INFO, "shutdown".to_string()).await;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn did_change_workspace_folders(&self, _: DidChangeWorkspaceFoldersParams) {
|
|
||||||
self.client
|
|
||||||
.log_message(MessageType::INFO, "workspace folders changed!")
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn did_change_configuration(&self, _: DidChangeConfigurationParams) {
|
|
||||||
self.client
|
|
||||||
.log_message(MessageType::INFO, "configuration changed!")
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn did_change_watched_files(&self, _: DidChangeWatchedFilesParams) {
|
|
||||||
self.client
|
|
||||||
.log_message(MessageType::INFO, "watched files have changed!")
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn did_open(&self, params: DidOpenTextDocumentParams) {
|
|
||||||
self.on_change(TextDocumentItem {
|
|
||||||
uri: params.text_document.uri,
|
|
||||||
text: params.text_document.text,
|
|
||||||
version: params.text_document.version,
|
|
||||||
language_id: params.text_document.language_id,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn did_change(&self, mut params: DidChangeTextDocumentParams) {
|
|
||||||
self.on_change(TextDocumentItem {
|
|
||||||
uri: params.text_document.uri,
|
|
||||||
text: std::mem::take(&mut params.content_changes[0].text),
|
|
||||||
version: params.text_document.version,
|
|
||||||
language_id: Default::default(),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn did_save(&self, params: DidSaveTextDocumentParams) {
|
|
||||||
if let Some(text) = params.text {
|
|
||||||
self.on_change(TextDocumentItem {
|
|
||||||
uri: params.text_document.uri,
|
|
||||||
text,
|
|
||||||
version: Default::default(),
|
|
||||||
language_id: Default::default(),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn did_close(&self, _: DidCloseTextDocumentParams) {
|
|
||||||
self.client.log_message(MessageType::INFO, "file closed!").await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn hover(&self, params: HoverParams) -> RpcResult<Option<Hover>> {
|
|
||||||
let filename = params.text_document_position_params.text_document.uri.to_string();
|
|
||||||
|
|
||||||
let Some(current_code) = self.current_code_map.get(&filename) else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
let pos = position_to_char_index(params.text_document_position_params.position, ¤t_code);
|
|
||||||
|
|
||||||
// Let's iterate over the AST and find the node that contains the cursor.
|
|
||||||
let Some(ast) = self.ast_map.get(&filename) else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(value) = ast.get_value_for_position(pos) else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(hover) = value.get_hover_value_for_position(pos, ¤t_code) else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
match hover {
|
|
||||||
crate::ast::types::Hover::Function { name, range } => {
|
|
||||||
// Get the docs for this function.
|
|
||||||
let Some(completion) = self.stdlib_completions.get(&name) else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
let Some(docs) = &completion.documentation else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
let docs = match docs {
|
|
||||||
Documentation::String(docs) => docs,
|
|
||||||
Documentation::MarkupContent(MarkupContent { value, .. }) => value,
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(label_details) = &completion.label_details else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Some(Hover {
|
|
||||||
contents: HoverContents::Markup(MarkupContent {
|
|
||||||
kind: MarkupKind::Markdown,
|
|
||||||
value: format!(
|
|
||||||
"```{}{}```\n{}",
|
|
||||||
name,
|
|
||||||
label_details.detail.clone().unwrap_or_default(),
|
|
||||||
docs
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
range: Some(range),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
crate::ast::types::Hover::Signature { .. } => Ok(None),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn completion(&self, params: CompletionParams) -> RpcResult<Option<CompletionResponse>> {
|
|
||||||
let mut completions = vec![CompletionItem {
|
|
||||||
label: PIPE_OPERATOR.to_string(),
|
|
||||||
label_details: None,
|
|
||||||
kind: Some(CompletionItemKind::OPERATOR),
|
|
||||||
detail: Some("A pipe operator.".to_string()),
|
|
||||||
documentation: Some(Documentation::MarkupContent(MarkupContent {
|
|
||||||
kind: MarkupKind::Markdown,
|
|
||||||
value: "A pipe operator.".to_string(),
|
|
||||||
})),
|
|
||||||
deprecated: Some(false),
|
|
||||||
preselect: None,
|
|
||||||
sort_text: None,
|
|
||||||
filter_text: None,
|
|
||||||
insert_text: Some("|> ".to_string()),
|
|
||||||
insert_text_format: Some(InsertTextFormat::PLAIN_TEXT),
|
|
||||||
insert_text_mode: None,
|
|
||||||
text_edit: None,
|
|
||||||
additional_text_edits: None,
|
|
||||||
command: None,
|
|
||||||
commit_characters: None,
|
|
||||||
data: None,
|
|
||||||
tags: None,
|
|
||||||
}];
|
|
||||||
|
|
||||||
completions.extend(self.stdlib_completions.values().cloned());
|
|
||||||
|
|
||||||
// Get our variables from our AST to include in our completions.
|
|
||||||
completions.extend(
|
|
||||||
self.completions_get_variables_from_ast(params.text_document_position.text_document.uri.as_ref())
|
|
||||||
.await,
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(Some(CompletionResponse::Array(completions)))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn diagnostic(&self, params: DocumentDiagnosticParams) -> RpcResult<DocumentDiagnosticReportResult> {
|
|
||||||
let filename = params.text_document.uri.to_string();
|
|
||||||
|
|
||||||
// Get the current diagnostics for this file.
|
|
||||||
let Some(diagnostic) = self.diagnostics_map.get(&filename) else {
|
|
||||||
// Send an empty report.
|
|
||||||
return Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
|
|
||||||
RelatedFullDocumentDiagnosticReport {
|
|
||||||
related_documents: None,
|
|
||||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
|
||||||
result_id: None,
|
|
||||||
items: vec![],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)));
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(DocumentDiagnosticReportResult::Report(diagnostic.clone()))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn signature_help(&self, params: SignatureHelpParams) -> RpcResult<Option<SignatureHelp>> {
|
|
||||||
let filename = params.text_document_position_params.text_document.uri.to_string();
|
|
||||||
|
|
||||||
let Some(current_code) = self.current_code_map.get(&filename) else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
let pos = position_to_char_index(params.text_document_position_params.position, ¤t_code);
|
|
||||||
|
|
||||||
// Let's iterate over the AST and find the node that contains the cursor.
|
|
||||||
let Some(ast) = self.ast_map.get(&filename) else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(value) = ast.get_value_for_position(pos) else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(hover) = value.get_hover_value_for_position(pos, ¤t_code) else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
match hover {
|
|
||||||
crate::ast::types::Hover::Function { name, range: _ } => {
|
|
||||||
// Get the docs for this function.
|
|
||||||
let Some(signature) = self.stdlib_signatures.get(&name) else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Some(signature.clone()))
|
|
||||||
}
|
|
||||||
crate::ast::types::Hover::Signature {
|
|
||||||
name,
|
|
||||||
parameter_index,
|
|
||||||
range: _,
|
|
||||||
} => {
|
|
||||||
let Some(signature) = self.stdlib_signatures.get(&name) else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut signature = signature.clone();
|
|
||||||
|
|
||||||
signature.active_parameter = Some(parameter_index);
|
|
||||||
|
|
||||||
Ok(Some(signature.clone()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn inlay_hint(&self, _params: InlayHintParams) -> RpcResult<Option<Vec<InlayHint>>> {
|
|
||||||
// TODO: do this
|
|
||||||
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn semantic_tokens_full(&self, params: SemanticTokensParams) -> RpcResult<Option<SemanticTokensResult>> {
|
|
||||||
let filename = params.text_document.uri.to_string();
|
|
||||||
|
|
||||||
let Some(semantic_tokens) = self.semantic_tokens_map.get(&filename) else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Some(SemanticTokensResult::Tokens(SemanticTokens {
|
|
||||||
result_id: None,
|
|
||||||
data: semantic_tokens.clone(),
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn document_symbol(&self, params: DocumentSymbolParams) -> RpcResult<Option<DocumentSymbolResponse>> {
|
|
||||||
let filename = params.text_document.uri.to_string();
|
|
||||||
|
|
||||||
let Some(symbols) = self.symbols_map.get(&filename) else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Some(DocumentSymbolResponse::Nested(symbols.clone())))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn formatting(&self, params: DocumentFormattingParams) -> RpcResult<Option<Vec<TextEdit>>> {
|
|
||||||
let filename = params.text_document.uri.to_string();
|
|
||||||
|
|
||||||
let Some(current_code) = self.current_code_map.get(&filename) else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Parse the ast.
|
|
||||||
// I don't know if we need to do this again since it should be updated in the context.
|
|
||||||
// But I figure better safe than sorry since this will write back out to the file.
|
|
||||||
let tokens = crate::token::lexer(¤t_code);
|
|
||||||
let parser = crate::parser::Parser::new(tokens);
|
|
||||||
let Ok(ast) = parser.ast() else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
// Now recast it.
|
|
||||||
let recast = ast.recast(
|
|
||||||
&crate::ast::types::FormatOptions {
|
|
||||||
tab_size: params.options.tab_size as usize,
|
|
||||||
insert_final_newline: params.options.insert_final_newline.unwrap_or(false),
|
|
||||||
use_tabs: !params.options.insert_spaces,
|
|
||||||
},
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
let source_range = SourceRange([0, current_code.len() - 1]);
|
|
||||||
let range = source_range.to_lsp_range(¤t_code);
|
|
||||||
Ok(Some(vec![TextEdit {
|
|
||||||
new_text: recast,
|
|
||||||
range,
|
|
||||||
}]))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn rename(&self, params: RenameParams) -> RpcResult<Option<WorkspaceEdit>> {
|
|
||||||
let filename = params.text_document_position.text_document.uri.to_string();
|
|
||||||
|
|
||||||
let Some(current_code) = self.current_code_map.get(&filename) else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Parse the ast.
|
|
||||||
// I don't know if we need to do this again since it should be updated in the context.
|
|
||||||
// But I figure better safe than sorry since this will write back out to the file.
|
|
||||||
let tokens = crate::token::lexer(¤t_code);
|
|
||||||
let parser = crate::parser::Parser::new(tokens);
|
|
||||||
let Ok(mut ast) = parser.ast() else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Let's convert the position to a character index.
|
|
||||||
let pos = position_to_char_index(params.text_document_position.position, ¤t_code);
|
|
||||||
// Now let's perform the rename on the ast.
|
|
||||||
ast.rename_symbol(¶ms.new_name, pos);
|
|
||||||
// Now recast it.
|
|
||||||
let recast = ast.recast(&Default::default(), 0);
|
|
||||||
let source_range = SourceRange([0, current_code.len() - 1]);
|
|
||||||
let range = source_range.to_lsp_range(¤t_code);
|
|
||||||
Ok(Some(WorkspaceEdit {
|
|
||||||
changes: Some(HashMap::from([(
|
|
||||||
params.text_document_position.text_document.uri,
|
|
||||||
vec![TextEdit {
|
|
||||||
new_text: recast,
|
|
||||||
range,
|
|
||||||
}],
|
|
||||||
)])),
|
|
||||||
document_changes: None,
|
|
||||||
change_annotations: None,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get completions from our stdlib.
|
|
||||||
pub fn get_completions_from_stdlib(stdlib: &crate::std::StdLib) -> Result<HashMap<String, CompletionItem>> {
|
|
||||||
let mut completions = HashMap::new();
|
|
||||||
|
|
||||||
for internal_fn in stdlib.fns.values() {
|
|
||||||
completions.insert(internal_fn.name(), internal_fn.to_completion_item());
|
|
||||||
}
|
|
||||||
|
|
||||||
let variable_kinds = VariableKind::to_completion_items()?;
|
|
||||||
for variable_kind in variable_kinds {
|
|
||||||
completions.insert(variable_kind.label.clone(), variable_kind);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(completions)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get signatures from our stdlib.
|
|
||||||
pub fn get_signatures_from_stdlib(stdlib: &crate::std::StdLib) -> Result<HashMap<String, SignatureHelp>> {
|
|
||||||
let mut signatures = HashMap::new();
|
|
||||||
|
|
||||||
for internal_fn in stdlib.fns.values() {
|
|
||||||
signatures.insert(internal_fn.name(), internal_fn.to_signature_help());
|
|
||||||
}
|
|
||||||
|
|
||||||
let show = SignatureHelp {
|
|
||||||
signatures: vec![SignatureInformation {
|
|
||||||
label: "show".to_string(),
|
|
||||||
documentation: Some(Documentation::MarkupContent(MarkupContent {
|
|
||||||
kind: MarkupKind::PlainText,
|
|
||||||
value: "Show a model.".to_string(),
|
|
||||||
})),
|
|
||||||
parameters: Some(vec![ParameterInformation {
|
|
||||||
label: ParameterLabel::Simple("sg: SketchGroup".to_string()),
|
|
||||||
documentation: Some(Documentation::MarkupContent(MarkupContent {
|
|
||||||
kind: MarkupKind::PlainText,
|
|
||||||
value: "A sketch group.".to_string(),
|
|
||||||
})),
|
|
||||||
}]),
|
|
||||||
active_parameter: None,
|
|
||||||
}],
|
|
||||||
active_signature: Some(0),
|
|
||||||
active_parameter: None,
|
|
||||||
};
|
|
||||||
signatures.insert("show".to_string(), show);
|
|
||||||
|
|
||||||
Ok(signatures)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert a position to a character index from the start of the file.
|
|
||||||
fn position_to_char_index(position: Position, code: &str) -> usize {
|
|
||||||
// Get the character position from the start of the file.
|
|
||||||
let mut char_position = 0;
|
|
||||||
for (index, line) in code.lines().enumerate() {
|
|
||||||
if index == position.line as usize {
|
|
||||||
char_position += position.character as usize;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
char_position += line.len() + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
char_position
|
|
||||||
}
|
|
||||||
|
33
src/wasm-lib/kcl/src/server/util.rs
Normal file
33
src/wasm-lib/kcl/src/server/util.rs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
//! Utility functions for working with ropes and positions.
|
||||||
|
|
||||||
|
use ropey::Rope;
|
||||||
|
use tower_lsp::lsp_types::Position;
|
||||||
|
|
||||||
|
pub fn position_to_offset(position: Position, rope: &Rope) -> Option<usize> {
|
||||||
|
Some(rope.try_line_to_char(position.line as usize).ok()? + position.character as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_text_before(offset: usize, rope: &Rope) -> Option<String> {
|
||||||
|
if offset == 0 {
|
||||||
|
return Some("".to_string());
|
||||||
|
}
|
||||||
|
Some(rope.slice(0..offset).to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_text_after(offset: usize, rope: &Rope) -> Option<String> {
|
||||||
|
let end_idx = rope.len_chars();
|
||||||
|
if offset == end_idx {
|
||||||
|
return Some("".to_string());
|
||||||
|
}
|
||||||
|
Some(rope.slice(offset..end_idx).to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_line_before(pos: Position, rope: &Rope) -> Option<String> {
|
||||||
|
let char_offset = pos.character as usize;
|
||||||
|
let offset = position_to_offset(pos, rope).unwrap();
|
||||||
|
if char_offset == 0 {
|
||||||
|
return Some("".to_string());
|
||||||
|
}
|
||||||
|
let line_start = offset - char_offset;
|
||||||
|
Some(rope.slice(line_start..offset).to_string())
|
||||||
|
}
|
@ -20,7 +20,6 @@ use parse_display::{Display, FromStr};
|
|||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use self::{kcl_stdlib::KclStdLibFn, sketch::SketchOnFaceTag};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ast::types::parse_json_number_as_f64,
|
ast::types::parse_json_number_as_f64,
|
||||||
docs::StdLibFn,
|
docs::StdLibFn,
|
||||||
@ -29,7 +28,10 @@ use crate::{
|
|||||||
executor::{
|
executor::{
|
||||||
ExecutorContext, ExtrudeGroup, Geometry, MemoryItem, Metadata, SketchGroup, SketchGroupSet, SourceRange,
|
ExecutorContext, ExtrudeGroup, Geometry, MemoryItem, Metadata, SketchGroup, SketchGroupSet, SourceRange,
|
||||||
},
|
},
|
||||||
std::sketch::SketchSurface,
|
std::{
|
||||||
|
kcl_stdlib::KclStdLibFn,
|
||||||
|
sketch::{SketchOnFaceTag, SketchSurface},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub type StdFn = fn(Args) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<MemoryItem, KclError>>>>;
|
pub type StdFn = fn(Args) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<MemoryItem, KclError>>>>;
|
||||||
|
@ -918,6 +918,7 @@ async fn start_sketch_on_face(
|
|||||||
animated: false,
|
animated: false,
|
||||||
ortho: false,
|
ortho: false,
|
||||||
entity_id: extrude_plane_id,
|
entity_id: extrude_plane_id,
|
||||||
|
adjust_camera: false,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
//! Wasm bindings for `kcl`.
|
//! Wasm bindings for `kcl`.
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
use futures::stream::TryStreamExt;
|
use futures::stream::TryStreamExt;
|
||||||
use gloo_utils::format::JsValueSerdeExt;
|
use gloo_utils::format::JsValueSerdeExt;
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
use kcl_lib::server::{get_completions_from_stdlib, get_signatures_from_stdlib, Backend};
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
use kcl_lib::std::utils;
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
use tower_lsp::{LspService, Server};
|
use tower_lsp::{LspService, Server};
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
@ -153,20 +152,20 @@ impl ServerConfig {
|
|||||||
// NOTE: input needs to be an AsyncIterator<Uint8Array, never, void> specifically
|
// NOTE: input needs to be an AsyncIterator<Uint8Array, never, void> specifically
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub async fn lsp_run(config: ServerConfig) -> Result<(), JsValue> {
|
pub async fn kcl_lsp_run(config: ServerConfig) -> Result<(), JsValue> {
|
||||||
let ServerConfig {
|
let ServerConfig {
|
||||||
into_server,
|
into_server,
|
||||||
from_server,
|
from_server,
|
||||||
} = config;
|
} = config;
|
||||||
|
|
||||||
let stdlib = kcl_lib::std::StdLib::new();
|
let stdlib = kcl_lib::std::StdLib::new();
|
||||||
let stdlib_completions = get_completions_from_stdlib(&stdlib).map_err(|e| e.to_string())?;
|
let stdlib_completions = kcl_lib::server::lsp::get_completions_from_stdlib(&stdlib).map_err(|e| e.to_string())?;
|
||||||
let stdlib_signatures = get_signatures_from_stdlib(&stdlib).map_err(|e| e.to_string())?;
|
let stdlib_signatures = kcl_lib::server::lsp::get_signatures_from_stdlib(&stdlib).map_err(|e| e.to_string())?;
|
||||||
// We can unwrap here because we know the tokeniser is valid, since
|
// We can unwrap here because we know the tokeniser is valid, since
|
||||||
// we have a test for it.
|
// we have a test for it.
|
||||||
let token_types = kcl_lib::token::TokenType::all_semantic_token_types().unwrap();
|
let token_types = kcl_lib::token::TokenType::all_semantic_token_types().unwrap();
|
||||||
|
|
||||||
let (service, socket) = LspService::new(|client| Backend {
|
let (service, socket) = LspService::new(|client| kcl_lib::server::lsp::Backend {
|
||||||
client,
|
client,
|
||||||
stdlib_completions,
|
stdlib_completions,
|
||||||
stdlib_signatures,
|
stdlib_signatures,
|
||||||
@ -199,10 +198,63 @@ pub async fn lsp_run(config: ServerConfig) -> Result<(), JsValue> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run the `copilot` lsp server.
|
||||||
|
//
|
||||||
|
// NOTE: we don't use web_sys::ReadableStream for input here because on the
|
||||||
|
// browser side we need to use a ReadableByteStreamController to construct it
|
||||||
|
// and so far only Chromium-based browsers support that functionality.
|
||||||
|
|
||||||
|
// NOTE: input needs to be an AsyncIterator<Uint8Array, never, void> specifically
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn copilot_lsp_run(config: ServerConfig, token: String) -> Result<(), JsValue> {
|
||||||
|
let ServerConfig {
|
||||||
|
into_server,
|
||||||
|
from_server,
|
||||||
|
} = config;
|
||||||
|
|
||||||
|
let (service, socket) = LspService::build(|client| kcl_lib::server::copilot::Backend {
|
||||||
|
client,
|
||||||
|
current_code_map: Default::default(),
|
||||||
|
editor_info: Arc::new(RwLock::new(
|
||||||
|
kcl_lib::server::copilot::types::CopilotEditorInfo::default(),
|
||||||
|
)),
|
||||||
|
cache: kcl_lib::server::copilot::cache::CopilotCache::new(),
|
||||||
|
token,
|
||||||
|
})
|
||||||
|
.custom_method("setEditorInfo", kcl_lib::server::copilot::Backend::set_editor_info)
|
||||||
|
.custom_method(
|
||||||
|
"getCompletions",
|
||||||
|
kcl_lib::server::copilot::Backend::get_completions_cycling,
|
||||||
|
)
|
||||||
|
.custom_method("notifyAccepted", kcl_lib::server::copilot::Backend::accept_completions)
|
||||||
|
.custom_method("notifyRejected", kcl_lib::server::copilot::Backend::reject_completions)
|
||||||
|
.finish();
|
||||||
|
|
||||||
|
let input = wasm_bindgen_futures::stream::JsStream::from(into_server);
|
||||||
|
let input = input
|
||||||
|
.map_ok(|value| {
|
||||||
|
value
|
||||||
|
.dyn_into::<js_sys::Uint8Array>()
|
||||||
|
.expect("could not cast stream item to Uint8Array")
|
||||||
|
.to_vec()
|
||||||
|
})
|
||||||
|
.map_err(|_err| std::io::Error::from(std::io::ErrorKind::Other))
|
||||||
|
.into_async_read();
|
||||||
|
|
||||||
|
let output = wasm_bindgen::JsCast::unchecked_into::<wasm_streams::writable::sys::WritableStream>(from_server);
|
||||||
|
let output = wasm_streams::WritableStream::from_raw(output);
|
||||||
|
let output = output.try_into_async_write().map_err(|err| err.0)?;
|
||||||
|
|
||||||
|
Server::new(input, output, socket).serve(service).await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn is_points_ccw(points: &[f64]) -> i32 {
|
pub fn is_points_ccw(points: &[f64]) -> i32 {
|
||||||
utils::is_points_ccw_wasm(points)
|
kcl_lib::std::utils::is_points_ccw_wasm(points)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
@ -237,7 +289,7 @@ pub fn get_tangential_arc_to_info(
|
|||||||
tan_previous_point_y: f64,
|
tan_previous_point_y: f64,
|
||||||
obtuse: bool,
|
obtuse: bool,
|
||||||
) -> TangentialArcInfoOutputWasm {
|
) -> TangentialArcInfoOutputWasm {
|
||||||
let result = utils::get_tangential_arc_to_info(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],
|
||||||
|
Reference in New Issue
Block a user