Code mirror plugin lsp interface (#1444)
* better named dirs Signed-off-by: Jess Frazelle <github@jessfraz.com> * move some stuff around Signed-off-by: Jess Frazelle <github@jessfraz.com> * more logging Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * less logging Signed-off-by: Jess Frazelle <github@jessfraz.com> * add fs in Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * file reader Signed-off-by: Jess Frazelle <github@jessfraz.com> * workspace Signed-off-by: Jess Frazelle <github@jessfraz.com> * start of workspace folders Signed-off-by: Jess Frazelle <github@jessfraz.com> * start of workspace folders Signed-off-by: Jess Frazelle <github@jessfraz.com> * cleanup workspace folders Signed-off-by: Jess Frazelle <github@jessfraz.com> * cleanup logs Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> --------- Signed-off-by: Jess Frazelle <github@jessfraz.com>
This commit is contained in:
@ -3,9 +3,9 @@ import ReactCodeMirror, {
|
|||||||
ViewUpdate,
|
ViewUpdate,
|
||||||
keymap,
|
keymap,
|
||||||
} from '@uiw/react-codemirror'
|
} from '@uiw/react-codemirror'
|
||||||
import { FromServer, IntoServer } from 'editor/lsp/codec'
|
import { FromServer, IntoServer } from 'editor/plugins/lsp/codec'
|
||||||
import Server from '../editor/lsp/server'
|
import Server from '../editor/plugins/lsp/server'
|
||||||
import Client from '../editor/lsp/client'
|
import Client from '../editor/plugins/lsp/client'
|
||||||
import { TEST } from 'env'
|
import { TEST } from 'env'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
@ -15,8 +15,8 @@ import { useMemo, useRef } from 'react'
|
|||||||
import { linter, lintGutter } from '@codemirror/lint'
|
import { linter, lintGutter } from '@codemirror/lint'
|
||||||
import { useStore } from 'useStore'
|
import { useStore } from 'useStore'
|
||||||
import { processCodeMirrorRanges } from 'lib/selections'
|
import { processCodeMirrorRanges } from 'lib/selections'
|
||||||
import { LanguageServerClient } from 'editor/lsp'
|
import { LanguageServerClient } from 'editor/plugins/lsp'
|
||||||
import kclLanguage from 'editor/lsp/language'
|
import kclLanguage from 'editor/plugins/lsp/kcl/language'
|
||||||
import { EditorView, lineHighlightField } from 'editor/highlightextension'
|
import { EditorView, lineHighlightField } from 'editor/highlightextension'
|
||||||
import { roundOff } from 'lib/utils'
|
import { roundOff } from 'lib/utils'
|
||||||
import { kclErrToDiagnostic } from 'lang/errors'
|
import { kclErrToDiagnostic } from 'lang/errors'
|
||||||
@ -27,7 +27,9 @@ 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'
|
import { copilotPlugin } from 'editor/plugins/lsp/copilot'
|
||||||
|
import { isTauri } from 'lib/isTauri'
|
||||||
|
import type * as LSP from 'vscode-languageserver-protocol'
|
||||||
|
|
||||||
export const editorShortcutMeta = {
|
export const editorShortcutMeta = {
|
||||||
formatCode: {
|
formatCode: {
|
||||||
@ -40,6 +42,15 @@ export const editorShortcutMeta = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
|
||||||
|
// We only use workspace folders in Tauri since that is where we use more than
|
||||||
|
// one file.
|
||||||
|
if (isTauri()) {
|
||||||
|
return [{ uri: 'file://', name: 'ProjectRoot' }]
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
export const TextEditor = ({
|
export const TextEditor = ({
|
||||||
theme,
|
theme,
|
||||||
}: {
|
}: {
|
||||||
@ -91,7 +102,7 @@ export const TextEditor = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const lspClient = new LanguageServerClient({ client })
|
const lspClient = new LanguageServerClient({ client, name: 'kcl' })
|
||||||
return { lspClient }
|
return { lspClient }
|
||||||
}, [setIsKclLspServerReady])
|
}, [setIsKclLspServerReady])
|
||||||
|
|
||||||
@ -107,7 +118,7 @@ export const TextEditor = ({
|
|||||||
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: getWorkspaceFolders(),
|
||||||
client: kclLspClient,
|
client: kclLspClient,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -128,7 +139,7 @@ export const TextEditor = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const lspClient = new LanguageServerClient({ client })
|
const lspClient = new LanguageServerClient({ client, name: 'copilot' })
|
||||||
return { lspClient }
|
return { lspClient }
|
||||||
}, [setIsCopilotLspServerReady])
|
}, [setIsCopilotLspServerReady])
|
||||||
|
|
||||||
@ -141,10 +152,10 @@ export const TextEditor = ({
|
|||||||
let plugin = null
|
let plugin = null
|
||||||
if (isCopilotLspServerReady && !TEST) {
|
if (isCopilotLspServerReady && !TEST) {
|
||||||
// Set up the lsp plugin.
|
// Set up the lsp plugin.
|
||||||
const lsp = copilotBundle({
|
const lsp = copilotPlugin({
|
||||||
// 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: getWorkspaceFolders(),
|
||||||
client: copilotLspClient,
|
client: copilotLspClient,
|
||||||
allowHTMLContent: true,
|
allowHTMLContent: true,
|
||||||
})
|
})
|
||||||
|
@ -65,6 +65,7 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
|
|||||||
afterInitializedHooks: (() => Promise<void>)[] = []
|
afterInitializedHooks: (() => Promise<void>)[] = []
|
||||||
#fromServer: FromServer
|
#fromServer: FromServer
|
||||||
private serverCapabilities: LSP.ServerCapabilities<any> = {}
|
private serverCapabilities: LSP.ServerCapabilities<any> = {}
|
||||||
|
private notifyFn: ((message: LSP.NotificationMessage) => void) | null = null
|
||||||
|
|
||||||
constructor(fromServer: FromServer, intoServer: IntoServer) {
|
constructor(fromServer: FromServer, intoServer: IntoServer) {
|
||||||
super(
|
super(
|
||||||
@ -167,9 +168,15 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
|
|||||||
return this.serverCapabilities
|
return this.serverCapabilities
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setNotifyFn(fn: (message: LSP.NotificationMessage) => void): void {
|
||||||
|
this.notifyFn = fn
|
||||||
|
}
|
||||||
|
|
||||||
async processNotifications(): Promise<void> {
|
async processNotifications(): Promise<void> {
|
||||||
for await (const notification of this.#fromServer.notifications) {
|
for await (const notification of this.#fromServer.notifications) {
|
||||||
await this.receiveAndSend(notification)
|
if (this.notifyFn) {
|
||||||
|
this.notifyFn(notification)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -11,30 +11,20 @@ import {
|
|||||||
Annotation,
|
Annotation,
|
||||||
EditorState,
|
EditorState,
|
||||||
Extension,
|
Extension,
|
||||||
Facet,
|
|
||||||
Prec,
|
Prec,
|
||||||
StateEffect,
|
StateEffect,
|
||||||
StateField,
|
StateField,
|
||||||
Transaction,
|
Transaction,
|
||||||
} from '@codemirror/state'
|
} from '@codemirror/state'
|
||||||
import { completionStatus } from '@codemirror/autocomplete'
|
import { completionStatus } from '@codemirror/autocomplete'
|
||||||
import { docPathFacet, offsetToPos, posToOffset } from 'editor/lsp/util'
|
import { offsetToPos, posToOffset } from 'editor/plugins/lsp/util'
|
||||||
import { LanguageServerPlugin } from 'editor/lsp/plugin'
|
import { LanguageServerOptions, LanguageServerClient } from 'editor/plugins/lsp'
|
||||||
import { LanguageServerOptions } from 'editor/lsp/plugin'
|
import {
|
||||||
import { LanguageServerClient } from 'editor/lsp'
|
LanguageServerPlugin,
|
||||||
|
documentUri,
|
||||||
// Create Facet for the current docPath
|
languageId,
|
||||||
export const docPath = Facet.define<string, string>({
|
workspaceFolders,
|
||||||
combine(value: readonly string[]) {
|
} from 'editor/plugins/lsp/plugin'
|
||||||
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' })
|
const ghostMark = Decoration.mark({ class: 'cm-ghostText' })
|
||||||
|
|
||||||
@ -361,9 +351,9 @@ const completionRequester = (client: LanguageServerClient) => {
|
|||||||
const pos = state.selection.main.head
|
const pos = state.selection.main.head
|
||||||
const source = state.doc.toString()
|
const source = state.doc.toString()
|
||||||
|
|
||||||
const path = state.facet(docPath)
|
const dUri = state.facet(documentUri)
|
||||||
const relativePath = state.facet(relDocPath)
|
const path = dUri.split('/').pop()!
|
||||||
const languageId = 'kcl'
|
const relativePath = dUri.replace('file://', '')
|
||||||
|
|
||||||
// Set a new timeout to request completion
|
// Set a new timeout to request completion
|
||||||
timeout = setTimeout(async () => {
|
timeout = setTimeout(async () => {
|
||||||
@ -378,9 +368,9 @@ const completionRequester = (client: LanguageServerClient) => {
|
|||||||
indentSize: 1,
|
indentSize: 1,
|
||||||
insertSpaces: true,
|
insertSpaces: true,
|
||||||
path,
|
path,
|
||||||
uri: `file://${path}`,
|
uri: dUri,
|
||||||
relativePath,
|
relativePath,
|
||||||
languageId,
|
languageId: state.facet(languageId),
|
||||||
position: offsetToPos(state.doc, pos),
|
position: offsetToPos(state.doc, pos),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -483,21 +473,24 @@ const completionRequester = (client: LanguageServerClient) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function copilotServer(options: LanguageServerOptions) {
|
export const copilotPlugin = (options: LanguageServerOptions): Extension => {
|
||||||
let plugin: LanguageServerPlugin
|
let plugin: LanguageServerPlugin | null = null
|
||||||
return ViewPlugin.define(
|
|
||||||
(view) =>
|
|
||||||
(plugin = new LanguageServerPlugin(view, options.allowHTMLContent))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const copilotBundle = (options: LanguageServerOptions): Extension => [
|
return [
|
||||||
docPath.of(options.documentUri.split('/').pop()!),
|
documentUri.of(options.documentUri),
|
||||||
docPathFacet.of(options.documentUri.split('/').pop()!),
|
languageId.of('kcl'),
|
||||||
relDocPath.of(options.documentUri.replace('file://', '')),
|
workspaceFolders.of(options.workspaceFolders),
|
||||||
completionDecoration,
|
ViewPlugin.define(
|
||||||
Prec.highest(completionPlugin(options.client)),
|
(view) =>
|
||||||
Prec.highest(viewCompletionPlugin(options.client)),
|
(plugin = new LanguageServerPlugin(
|
||||||
completionRequester(options.client),
|
options.client,
|
||||||
copilotServer(options),
|
view,
|
||||||
]
|
options.allowHTMLContent
|
||||||
|
))
|
||||||
|
),
|
||||||
|
completionDecoration,
|
||||||
|
Prec.highest(completionPlugin(options.client)),
|
||||||
|
Prec.highest(viewCompletionPlugin(options.client)),
|
||||||
|
completionRequester(options.client),
|
||||||
|
]
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import type * as LSP from 'vscode-languageserver-protocol'
|
import type * as LSP from 'vscode-languageserver-protocol'
|
||||||
import Client from './client'
|
import Client from './client'
|
||||||
import { LanguageServerPlugin } from './plugin'
|
import { SemanticToken, deserializeTokens } from './kcl/semantic_tokens'
|
||||||
import { SemanticToken, deserializeTokens } from './semantic_tokens'
|
import { LanguageServerPlugin } from 'editor/plugins/lsp/plugin'
|
||||||
|
|
||||||
export interface CopilotGetCompletionsParams {
|
export interface CopilotGetCompletionsParams {
|
||||||
doc: {
|
doc: {
|
||||||
@ -90,26 +90,22 @@ interface LSPNotifyMap {
|
|||||||
'textDocument/didOpen': LSP.DidOpenTextDocumentParams
|
'textDocument/didOpen': LSP.DidOpenTextDocumentParams
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server to client
|
|
||||||
interface LSPEventMap {
|
|
||||||
'textDocument/publishDiagnostics': LSP.PublishDiagnosticsParams
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Notification = {
|
|
||||||
[key in keyof LSPEventMap]: {
|
|
||||||
jsonrpc: '2.0'
|
|
||||||
id?: null | undefined
|
|
||||||
method: key
|
|
||||||
params: LSPEventMap[key]
|
|
||||||
}
|
|
||||||
}[keyof LSPEventMap]
|
|
||||||
|
|
||||||
export interface LanguageServerClientOptions {
|
export interface LanguageServerClientOptions {
|
||||||
client: Client
|
client: Client
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LanguageServerOptions {
|
||||||
|
// We assume this is the main project directory, we are currently working in.
|
||||||
|
workspaceFolders: LSP.WorkspaceFolder[]
|
||||||
|
documentUri: string
|
||||||
|
allowHTMLContent: boolean
|
||||||
|
client: LanguageServerClient
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LanguageServerClient {
|
export class LanguageServerClient {
|
||||||
private client: Client
|
private client: Client
|
||||||
|
private name: string
|
||||||
|
|
||||||
public ready: boolean
|
public ready: boolean
|
||||||
|
|
||||||
@ -124,6 +120,7 @@ export class LanguageServerClient {
|
|||||||
constructor(options: LanguageServerClientOptions) {
|
constructor(options: LanguageServerClientOptions) {
|
||||||
this.plugins = []
|
this.plugins = []
|
||||||
this.client = options.client
|
this.client = options.client
|
||||||
|
this.name = options.name
|
||||||
|
|
||||||
this.ready = false
|
this.ready = false
|
||||||
|
|
||||||
@ -133,11 +130,16 @@ export class LanguageServerClient {
|
|||||||
|
|
||||||
async initialize() {
|
async initialize() {
|
||||||
// Start the client in the background.
|
// Start the client in the background.
|
||||||
|
this.client.setNotifyFn(this.processNotifications.bind(this))
|
||||||
this.client.start()
|
this.client.start()
|
||||||
|
|
||||||
this.ready = true
|
this.ready = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getName(): string {
|
||||||
|
return this.name
|
||||||
|
}
|
||||||
|
|
||||||
getServerCapabilities(): LSP.ServerCapabilities<any> {
|
getServerCapabilities(): LSP.ServerCapabilities<any> {
|
||||||
return this.client.getServerCapabilities()
|
return this.client.getServerCapabilities()
|
||||||
}
|
}
|
||||||
@ -156,6 +158,11 @@ export class LanguageServerClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateSemanticTokens(uri: string) {
|
async updateSemanticTokens(uri: string) {
|
||||||
|
const serverCapabilities = this.getServerCapabilities()
|
||||||
|
if (!serverCapabilities.semanticTokensProvider) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure we can only run, if we aren't already running.
|
// Make sure we can only run, if we aren't already running.
|
||||||
if (!this.isUpdatingSemanticTokens) {
|
if (!this.isUpdatingSemanticTokens) {
|
||||||
this.isUpdatingSemanticTokens = true
|
this.isUpdatingSemanticTokens = true
|
||||||
@ -180,10 +187,18 @@ export class LanguageServerClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async textDocumentHover(params: LSP.HoverParams) {
|
async textDocumentHover(params: LSP.HoverParams) {
|
||||||
|
const serverCapabilities = this.getServerCapabilities()
|
||||||
|
if (!serverCapabilities.hoverProvider) {
|
||||||
|
return
|
||||||
|
}
|
||||||
return await this.request('textDocument/hover', params)
|
return await this.request('textDocument/hover', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
async textDocumentCompletion(params: LSP.CompletionParams) {
|
async textDocumentCompletion(params: LSP.CompletionParams) {
|
||||||
|
const serverCapabilities = this.getServerCapabilities()
|
||||||
|
if (!serverCapabilities.completionProvider) {
|
||||||
|
return
|
||||||
|
}
|
||||||
return await this.request('textDocument/completion', params)
|
return await this.request('textDocument/completion', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,11 +249,12 @@ export class LanguageServerClient {
|
|||||||
async acceptCompletion(params: CopilotAcceptCompletionParams) {
|
async acceptCompletion(params: CopilotAcceptCompletionParams) {
|
||||||
return await this.request('notifyAccepted', params)
|
return await this.request('notifyAccepted', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
async rejectCompletions(params: CopilotRejectCompletionParams) {
|
async rejectCompletions(params: CopilotRejectCompletionParams) {
|
||||||
return await this.request('notifyRejected', params)
|
return await this.request('notifyRejected', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
private processNotification(notification: Notification) {
|
private processNotifications(notification: LSP.NotificationMessage) {
|
||||||
for (const plugin of this.plugins) plugin.processNotification(notification)
|
for (const plugin of this.plugins) plugin.processNotification(notification)
|
||||||
}
|
}
|
||||||
}
|
}
|
75
src/editor/plugins/lsp/kcl/index.ts
Normal file
75
src/editor/plugins/lsp/kcl/index.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { autocompletion } from '@codemirror/autocomplete'
|
||||||
|
import { Extension } from '@codemirror/state'
|
||||||
|
import { ViewPlugin, hoverTooltip, tooltips } from '@codemirror/view'
|
||||||
|
import { CompletionTriggerKind } from 'vscode-languageserver-protocol'
|
||||||
|
import { offsetToPos } from 'editor/plugins/lsp/util'
|
||||||
|
import { LanguageServerOptions } from 'editor/plugins/lsp'
|
||||||
|
import {
|
||||||
|
LanguageServerPlugin,
|
||||||
|
documentUri,
|
||||||
|
languageId,
|
||||||
|
workspaceFolders,
|
||||||
|
} from 'editor/plugins/lsp/plugin'
|
||||||
|
|
||||||
|
export function kclPlugin(options: LanguageServerOptions): Extension {
|
||||||
|
let plugin: LanguageServerPlugin | null = null
|
||||||
|
|
||||||
|
return [
|
||||||
|
documentUri.of(options.documentUri),
|
||||||
|
languageId.of('kcl'),
|
||||||
|
workspaceFolders.of(options.workspaceFolders),
|
||||||
|
ViewPlugin.define(
|
||||||
|
(view) =>
|
||||||
|
(plugin = new LanguageServerPlugin(
|
||||||
|
options.client,
|
||||||
|
view,
|
||||||
|
options.allowHTMLContent
|
||||||
|
))
|
||||||
|
),
|
||||||
|
hoverTooltip(
|
||||||
|
(view, pos) =>
|
||||||
|
plugin?.requestHoverTooltip(view, offsetToPos(view.state.doc, pos)) ??
|
||||||
|
null
|
||||||
|
),
|
||||||
|
tooltips({
|
||||||
|
position: 'absolute',
|
||||||
|
}),
|
||||||
|
autocompletion({
|
||||||
|
override: [
|
||||||
|
async (context) => {
|
||||||
|
if (plugin == null) return null
|
||||||
|
|
||||||
|
const { state, pos, explicit } = context
|
||||||
|
const line = state.doc.lineAt(pos)
|
||||||
|
let trigKind: CompletionTriggerKind = CompletionTriggerKind.Invoked
|
||||||
|
let trigChar: string | undefined
|
||||||
|
if (
|
||||||
|
!explicit &&
|
||||||
|
plugin.client
|
||||||
|
.getServerCapabilities()
|
||||||
|
.completionProvider?.triggerCharacters?.includes(
|
||||||
|
line.text[pos - line.from - 1]
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
trigKind = CompletionTriggerKind.TriggerCharacter
|
||||||
|
trigChar = line.text[pos - line.from - 1]
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
trigKind === CompletionTriggerKind.Invoked &&
|
||||||
|
!context.matchBefore(/\w+$/)
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return await plugin.requestCompletion(
|
||||||
|
context,
|
||||||
|
offsetToPos(state.doc, pos),
|
||||||
|
{
|
||||||
|
triggerKind: trigKind,
|
||||||
|
triggerCharacter: trigChar,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
@ -5,8 +5,8 @@ import {
|
|||||||
defineLanguageFacet,
|
defineLanguageFacet,
|
||||||
LanguageSupport,
|
LanguageSupport,
|
||||||
} from '@codemirror/language'
|
} from '@codemirror/language'
|
||||||
import { LanguageServerClient } from '.'
|
import { LanguageServerClient } from 'editor/plugins/lsp'
|
||||||
import { kclPlugin } from './plugin'
|
import { kclPlugin } from '.'
|
||||||
import type * as LSP from 'vscode-languageserver-protocol'
|
import type * as LSP from 'vscode-languageserver-protocol'
|
||||||
import { parser as jsParser } from '@lezer/javascript'
|
import { parser as jsParser } from '@lezer/javascript'
|
||||||
import { EditorState } from '@uiw/react-codemirror'
|
import { EditorState } from '@uiw/react-codemirror'
|
||||||
@ -14,7 +14,7 @@ import { EditorState } from '@uiw/react-codemirror'
|
|||||||
const data = defineLanguageFacet({})
|
const data = defineLanguageFacet({})
|
||||||
|
|
||||||
export interface LanguageOptions {
|
export interface LanguageOptions {
|
||||||
workspaceFolders: LSP.WorkspaceFolder[] | null
|
workspaceFolders: LSP.WorkspaceFolder[]
|
||||||
documentUri: string
|
documentUri: string
|
||||||
client: LanguageServerClient
|
client: LanguageServerClient
|
||||||
}
|
}
|
@ -9,8 +9,8 @@ import {
|
|||||||
NodeType,
|
NodeType,
|
||||||
NodeSet,
|
NodeSet,
|
||||||
} from '@lezer/common'
|
} from '@lezer/common'
|
||||||
import { LanguageServerClient } from '.'
|
import { LanguageServerClient } from 'editor/plugins/lsp'
|
||||||
import { posToOffset } from 'editor/lsp/util'
|
import { posToOffset } from 'editor/plugins/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'
|
@ -1,13 +1,7 @@
|
|||||||
import { autocompletion, completeFromList } from '@codemirror/autocomplete'
|
import { completeFromList } from '@codemirror/autocomplete'
|
||||||
import { setDiagnostics } from '@codemirror/lint'
|
import { setDiagnostics } from '@codemirror/lint'
|
||||||
import { Facet } from '@codemirror/state'
|
import { Facet } from '@codemirror/state'
|
||||||
import {
|
import { EditorView, Tooltip } from '@codemirror/view'
|
||||||
EditorView,
|
|
||||||
ViewPlugin,
|
|
||||||
Tooltip,
|
|
||||||
hoverTooltip,
|
|
||||||
tooltips,
|
|
||||||
} from '@codemirror/view'
|
|
||||||
import {
|
import {
|
||||||
DiagnosticSeverity,
|
DiagnosticSeverity,
|
||||||
CompletionItemKind,
|
CompletionItemKind,
|
||||||
@ -23,9 +17,17 @@ import type {
|
|||||||
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 * as LSP from 'vscode-languageserver-protocol'
|
import type * as LSP from 'vscode-languageserver-protocol'
|
||||||
import { LanguageServerClient, Notification } from '.'
|
import { LanguageServerClient } from 'editor/plugins/lsp'
|
||||||
import { Marked } from '@ts-stack/markdown'
|
import { Marked } from '@ts-stack/markdown'
|
||||||
import { offsetToPos, posToOffset } from 'editor/lsp/util'
|
import { posToOffset } from 'editor/plugins/lsp/util'
|
||||||
|
|
||||||
|
const useLast = (values: readonly any[]) => values.reduce((_, v) => v, '')
|
||||||
|
export const documentUri = Facet.define<string, string>({ combine: useLast })
|
||||||
|
export const languageId = Facet.define<string, string>({ combine: useLast })
|
||||||
|
export const workspaceFolders = Facet.define<
|
||||||
|
LSP.WorkspaceFolder[],
|
||||||
|
LSP.WorkspaceFolder[]
|
||||||
|
>({ combine: useLast })
|
||||||
|
|
||||||
const changesDelay = 500
|
const changesDelay = 500
|
||||||
|
|
||||||
@ -33,31 +35,22 @@ const CompletionItemKindMap = Object.fromEntries(
|
|||||||
Object.entries(CompletionItemKind).map(([key, value]) => [value, key])
|
Object.entries(CompletionItemKind).map(([key, value]) => [value, key])
|
||||||
) as Record<CompletionItemKind, string>
|
) as Record<CompletionItemKind, string>
|
||||||
|
|
||||||
const useLast = (values: readonly any[]) => values.reduce((_, v) => v, '')
|
|
||||||
const documentUri = Facet.define<string, string>({ combine: useLast })
|
|
||||||
const languageId = Facet.define<string, string>({ combine: useLast })
|
|
||||||
const client = Facet.define<LanguageServerClient, LanguageServerClient>({
|
|
||||||
combine: useLast,
|
|
||||||
})
|
|
||||||
|
|
||||||
export interface LanguageServerOptions {
|
|
||||||
workspaceFolders: LSP.WorkspaceFolder[] | null
|
|
||||||
documentUri: string
|
|
||||||
allowHTMLContent: boolean
|
|
||||||
client: LanguageServerClient
|
|
||||||
}
|
|
||||||
|
|
||||||
export class LanguageServerPlugin implements PluginValue {
|
export class LanguageServerPlugin implements PluginValue {
|
||||||
public client: LanguageServerClient
|
public client: LanguageServerClient
|
||||||
|
|
||||||
private documentUri: string
|
private documentUri: string
|
||||||
private languageId: string
|
private languageId: string
|
||||||
|
private workspaceFolders: LSP.WorkspaceFolder[]
|
||||||
private documentVersion: number
|
private documentVersion: number
|
||||||
|
|
||||||
constructor(private view: EditorView, private allowHTMLContent: boolean) {
|
constructor(
|
||||||
this.client = this.view.state.facet(client)
|
client: LanguageServerClient,
|
||||||
|
private view: EditorView,
|
||||||
|
private allowHTMLContent: boolean
|
||||||
|
) {
|
||||||
|
this.client = client
|
||||||
this.documentUri = this.view.state.facet(documentUri)
|
this.documentUri = this.view.state.facet(documentUri)
|
||||||
this.languageId = this.view.state.facet(languageId)
|
this.languageId = this.view.state.facet(languageId)
|
||||||
|
this.workspaceFolders = this.view.state.facet(workspaceFolders)
|
||||||
this.documentVersion = 0
|
this.documentVersion = 0
|
||||||
|
|
||||||
this.client.attachPlugin(this)
|
this.client.attachPlugin(this)
|
||||||
@ -238,11 +231,28 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
return completeFromList(options)(context)
|
return completeFromList(options)(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
processNotification(notification: Notification) {
|
processNotification(notification: LSP.NotificationMessage) {
|
||||||
try {
|
try {
|
||||||
switch (notification.method) {
|
switch (notification.method) {
|
||||||
case 'textDocument/publishDiagnostics':
|
case 'textDocument/publishDiagnostics':
|
||||||
this.processDiagnostics(notification.params)
|
this.processDiagnostics(
|
||||||
|
notification.params as PublishDiagnosticsParams
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case 'window/logMessage':
|
||||||
|
console.log(
|
||||||
|
'[lsp] [window/logMessage]',
|
||||||
|
this.client.getName(),
|
||||||
|
notification.params
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case 'window/showMessage':
|
||||||
|
console.log(
|
||||||
|
'[lsp] [window/showMessage]',
|
||||||
|
this.client.getName(),
|
||||||
|
notification.params
|
||||||
|
)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
@ -284,65 +294,6 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function kclPlugin(options: LanguageServerOptions) {
|
|
||||||
let plugin: LanguageServerPlugin | null = null
|
|
||||||
|
|
||||||
return [
|
|
||||||
client.of(options.client),
|
|
||||||
documentUri.of(options.documentUri),
|
|
||||||
languageId.of('kcl'),
|
|
||||||
ViewPlugin.define(
|
|
||||||
(view) =>
|
|
||||||
(plugin = new LanguageServerPlugin(view, options.allowHTMLContent))
|
|
||||||
),
|
|
||||||
hoverTooltip(
|
|
||||||
(view, pos) =>
|
|
||||||
plugin?.requestHoverTooltip(view, offsetToPos(view.state.doc, pos)) ??
|
|
||||||
null
|
|
||||||
),
|
|
||||||
tooltips({
|
|
||||||
position: 'absolute',
|
|
||||||
}),
|
|
||||||
autocompletion({
|
|
||||||
override: [
|
|
||||||
async (context) => {
|
|
||||||
if (plugin == null) return null
|
|
||||||
|
|
||||||
const { state, pos, explicit } = context
|
|
||||||
const line = state.doc.lineAt(pos)
|
|
||||||
let trigKind: CompletionTriggerKind = CompletionTriggerKind.Invoked
|
|
||||||
let trigChar: string | undefined
|
|
||||||
if (
|
|
||||||
!explicit &&
|
|
||||||
plugin.client
|
|
||||||
.getServerCapabilities()
|
|
||||||
.completionProvider?.triggerCharacters?.includes(
|
|
||||||
line.text[pos - line.from - 1]
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
trigKind = CompletionTriggerKind.TriggerCharacter
|
|
||||||
trigChar = line.text[pos - line.from - 1]
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
trigKind === CompletionTriggerKind.Invoked &&
|
|
||||||
!context.matchBefore(/\w+$/)
|
|
||||||
) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return await plugin.requestCompletion(
|
|
||||||
context,
|
|
||||||
offsetToPos(state.doc, pos),
|
|
||||||
{
|
|
||||||
triggerKind: trigKind,
|
|
||||||
triggerCharacter: trigChar,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatContents(
|
function formatContents(
|
||||||
contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[]
|
contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[]
|
||||||
): string {
|
): string {
|
@ -34,6 +34,8 @@ const ServerCapabilitiesProviders: IMethodServerCapabilityProviderDictionary = {
|
|||||||
'textDocument/foldingRange': 'foldingRangeProvider',
|
'textDocument/foldingRange': 'foldingRangeProvider',
|
||||||
'textDocument/declaration': 'declarationProvider',
|
'textDocument/declaration': 'declarationProvider',
|
||||||
'textDocument/executeCommand': 'executeCommandProvider',
|
'textDocument/executeCommand': 'executeCommandProvider',
|
||||||
|
'textDocument/semanticTokens/full': 'semanticTokensProvider',
|
||||||
|
'textDocument/publishDiagnostics': 'diagnosticsProvider',
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerServerCapability(
|
function registerServerCapability(
|
@ -3,8 +3,9 @@ import init, {
|
|||||||
InitOutput,
|
InitOutput,
|
||||||
kcl_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'
|
||||||
|
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
||||||
|
|
||||||
export default class Server {
|
export default class Server {
|
||||||
readonly initOutput: InitOutput
|
readonly initOutput: InitOutput
|
||||||
@ -31,7 +32,11 @@ export default class Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async start(type_: 'kcl' | 'copilot', token?: string): 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,
|
||||||
|
fileSystemManager
|
||||||
|
)
|
||||||
if (type_ === 'copilot') {
|
if (type_ === 'copilot') {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new Error('auth token is required for copilot')
|
throw new Error('auth token is required for copilot')
|
@ -1,4 +1,4 @@
|
|||||||
import { Facet, Text } from '@codemirror/state'
|
import { Text } from '@codemirror/state'
|
||||||
|
|
||||||
export function posToOffset(
|
export function posToOffset(
|
||||||
doc: Text,
|
doc: Text,
|
||||||
@ -17,7 +17,3 @@ export function offsetToPos(doc: Text, offset: number) {
|
|||||||
character: offset - line.from,
|
character: offset - line.from,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const docPathFacet = Facet.define<string, string>({
|
|
||||||
combine: (values) => values[values.length - 1],
|
|
||||||
})
|
|
@ -1,4 +1,8 @@
|
|||||||
import { readBinaryFile, exists as tauriExists } from '@tauri-apps/api/fs'
|
import {
|
||||||
|
readDir,
|
||||||
|
readBinaryFile,
|
||||||
|
exists as tauriExists,
|
||||||
|
} from '@tauri-apps/api/fs'
|
||||||
import { isTauri } from 'lib/isTauri'
|
import { isTauri } from 'lib/isTauri'
|
||||||
import { join } from '@tauri-apps/api/path'
|
import { join } from '@tauri-apps/api/path'
|
||||||
|
|
||||||
@ -53,6 +57,30 @@ class FileSystemManager {
|
|||||||
return tauriExists(file)
|
return tauriExists(file)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAllFiles(path: string): Promise<string[] | void> {
|
||||||
|
// Using local file system only works from Tauri.
|
||||||
|
if (!isTauri()) {
|
||||||
|
throw new Error(
|
||||||
|
'This function can only be called from a Tauri application'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return join(this.dir, path)
|
||||||
|
.catch((error) => {
|
||||||
|
throw new Error(`Error joining dir: ${error}`)
|
||||||
|
})
|
||||||
|
.then((p) => {
|
||||||
|
readDir(p, { recursive: true })
|
||||||
|
.catch((error) => {
|
||||||
|
throw new Error(`Error reading dir: ${error}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
.then((files) => {
|
||||||
|
return files.map((file) => file.path)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fileSystemManager = new FileSystemManager()
|
export const fileSystemManager = new FileSystemManager()
|
||||||
|
@ -37,7 +37,10 @@ export type Events =
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY'
|
export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY'
|
||||||
const persistedToken = localStorage?.getItem(TOKEN_PERSIST_KEY) || ''
|
const persistedToken =
|
||||||
|
localStorage?.getItem(TOKEN_PERSIST_KEY) ||
|
||||||
|
getCookie('__Secure-next-auth.session-token') ||
|
||||||
|
''
|
||||||
|
|
||||||
export const authMachine = createMachine<UserContext, Events>(
|
export const authMachine = createMachine<UserContext, Events>(
|
||||||
{
|
{
|
||||||
@ -135,3 +138,23 @@ async function getUser(context: UserContext) {
|
|||||||
|
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCookie(cname: string): string {
|
||||||
|
if (isTauri()) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = cname + '='
|
||||||
|
let decodedCookie = decodeURIComponent(document.cookie)
|
||||||
|
let ca = decodedCookie.split(';')
|
||||||
|
for (let i = 0; i < ca.length; i++) {
|
||||||
|
let c = ca[i]
|
||||||
|
while (c.charAt(0) === ' ') {
|
||||||
|
c = c.substring(1)
|
||||||
|
}
|
||||||
|
if (c.indexOf(name) === 0) {
|
||||||
|
return c.substring(name.length, c.length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
@ -53,4 +53,38 @@ impl FileSystem for FileManager {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_all_files<P: AsRef<std::path::Path>>(
|
||||||
|
&self,
|
||||||
|
path: P,
|
||||||
|
source_range: crate::executor::SourceRange,
|
||||||
|
) -> Result<Vec<std::path::PathBuf>, crate::errors::KclError> {
|
||||||
|
let mut files = vec![];
|
||||||
|
let mut stack = vec![path.as_ref().to_path_buf()];
|
||||||
|
|
||||||
|
while let Some(path) = stack.pop() {
|
||||||
|
if !path.is_dir() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut read_dir = tokio::fs::read_dir(&path).await.map_err(|e| {
|
||||||
|
KclError::Engine(KclErrorDetails {
|
||||||
|
message: format!("Failed to read directory `{}`: {}", path.display(), e),
|
||||||
|
source_ranges: vec![source_range],
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
while let Ok(Some(entry)) = read_dir.next_entry().await {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
// Iterate over the directory.
|
||||||
|
stack.push(path);
|
||||||
|
} else {
|
||||||
|
files.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(files)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,4 +28,11 @@ pub trait FileSystem: Clone {
|
|||||||
path: P,
|
path: P,
|
||||||
source_range: crate::executor::SourceRange,
|
source_range: crate::executor::SourceRange,
|
||||||
) -> Result<bool, crate::errors::KclError>;
|
) -> Result<bool, crate::errors::KclError>;
|
||||||
|
|
||||||
|
/// Get all the files in a directory recursively.
|
||||||
|
async fn get_all_files<P: AsRef<std::path::Path>>(
|
||||||
|
&self,
|
||||||
|
path: P,
|
||||||
|
source_range: crate::executor::SourceRange,
|
||||||
|
) -> Result<Vec<std::path::PathBuf>, crate::errors::KclError>;
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,9 @@ extern "C" {
|
|||||||
|
|
||||||
#[wasm_bindgen(method, js_name = exists, catch)]
|
#[wasm_bindgen(method, js_name = exists, catch)]
|
||||||
fn exists(this: &FileSystemManager, path: String) -> Result<js_sys::Promise, js_sys::Error>;
|
fn exists(this: &FileSystemManager, path: String) -> Result<js_sys::Promise, js_sys::Error>;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = getAllFiles, catch)]
|
||||||
|
fn get_all_files(this: &FileSystemManager, path: String) -> Result<js_sys::Promise, js_sys::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -31,6 +34,9 @@ impl FileManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for FileManager {}
|
||||||
|
unsafe impl Sync for FileManager {}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl FileSystem for FileManager {
|
impl FileSystem for FileManager {
|
||||||
async fn read<P: AsRef<std::path::Path>>(
|
async fn read<P: AsRef<std::path::Path>>(
|
||||||
@ -112,4 +118,53 @@ impl FileSystem for FileManager {
|
|||||||
|
|
||||||
Ok(it_exists)
|
Ok(it_exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_all_files<P: AsRef<std::path::Path>>(
|
||||||
|
&self,
|
||||||
|
path: P,
|
||||||
|
source_range: crate::executor::SourceRange,
|
||||||
|
) -> Result<Vec<std::path::PathBuf>, crate::errors::KclError> {
|
||||||
|
let promise = self
|
||||||
|
.manager
|
||||||
|
.get_all_files(
|
||||||
|
path.as_ref()
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
KclError::Engine(KclErrorDetails {
|
||||||
|
message: "Failed to convert path to string".to_string(),
|
||||||
|
source_ranges: vec![source_range],
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
.map_err(|e| {
|
||||||
|
KclError::Engine(KclErrorDetails {
|
||||||
|
message: e.to_string().into(),
|
||||||
|
source_ranges: vec![source_range],
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let value = wasm_bindgen_futures::JsFuture::from(promise).await.map_err(|e| {
|
||||||
|
KclError::Engine(KclErrorDetails {
|
||||||
|
message: format!("Failed to wait for promise from javascript: {:?}", e),
|
||||||
|
source_ranges: vec![source_range],
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let s = value.as_string().ok_or_else(|| {
|
||||||
|
KclError::Engine(KclErrorDetails {
|
||||||
|
message: format!("Failed to get string from response from javascript: `{:?}`", value),
|
||||||
|
source_ranges: vec![source_range],
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let files: Vec<String> = serde_json::from_str(&s).map_err(|e| {
|
||||||
|
KclError::Engine(KclErrorDetails {
|
||||||
|
message: format!("Failed to parse json from javascript: `{}` `{:?}`", s, e),
|
||||||
|
source_ranges: vec![source_range],
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(files.into_iter().map(|s| std::path::PathBuf::from(s)).collect())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ pub mod engine;
|
|||||||
pub mod errors;
|
pub mod errors;
|
||||||
pub mod executor;
|
pub mod executor;
|
||||||
pub mod fs;
|
pub mod fs;
|
||||||
|
pub mod lsp;
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
pub mod server;
|
|
||||||
pub mod std;
|
pub mod std;
|
||||||
pub mod token;
|
pub mod token;
|
||||||
|
@ -13,6 +13,8 @@ use tower_lsp::lsp_types::{
|
|||||||
pub trait Backend {
|
pub trait Backend {
|
||||||
fn client(&self) -> tower_lsp::Client;
|
fn client(&self) -> tower_lsp::Client;
|
||||||
|
|
||||||
|
fn fs(&self) -> crate::fs::FileManager;
|
||||||
|
|
||||||
/// Get the current code map.
|
/// Get the current code map.
|
||||||
fn current_code_map(&self) -> DashMap<String, String>;
|
fn current_code_map(&self) -> DashMap<String, String>;
|
||||||
|
|
@ -6,7 +6,7 @@ use std::{
|
|||||||
sync::{Mutex, RwLock},
|
sync::{Mutex, RwLock},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::server::copilot::types::CopilotCompletionResponse;
|
use crate::lsp::copilot::types::CopilotCompletionResponse;
|
||||||
|
|
||||||
// if file changes, keep the cache.
|
// if file changes, keep the cache.
|
||||||
// if line number is different for an existing file, clean.
|
// if line number is different for an existing file, clean.
|
@ -23,7 +23,7 @@ use tower_lsp::{
|
|||||||
LanguageServer,
|
LanguageServer,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::server::{
|
use crate::lsp::{
|
||||||
backend::Backend as _,
|
backend::Backend as _,
|
||||||
copilot::types::{CopilotCompletionResponse, CopilotEditorInfo, CopilotLspCompletionParams, DocParams},
|
copilot::types::{CopilotCompletionResponse, CopilotEditorInfo, CopilotLspCompletionParams, DocParams},
|
||||||
};
|
};
|
||||||
@ -42,6 +42,8 @@ impl Success {
|
|||||||
pub struct Backend {
|
pub struct Backend {
|
||||||
/// The client is used to send notifications and requests to the client.
|
/// The client is used to send notifications and requests to the client.
|
||||||
pub client: tower_lsp::Client,
|
pub client: tower_lsp::Client,
|
||||||
|
/// The file system client to use.
|
||||||
|
pub fs: crate::fs::FileManager,
|
||||||
/// Current code.
|
/// Current code.
|
||||||
pub current_code_map: DashMap<String, String>,
|
pub current_code_map: DashMap<String, String>,
|
||||||
/// The token is used to authenticate requests to the API server.
|
/// The token is used to authenticate requests to the API server.
|
||||||
@ -54,11 +56,15 @@ pub struct Backend {
|
|||||||
|
|
||||||
// Implement the shared backend trait for the language server.
|
// Implement the shared backend trait for the language server.
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl crate::server::backend::Backend for Backend {
|
impl crate::lsp::backend::Backend for Backend {
|
||||||
fn client(&self) -> tower_lsp::Client {
|
fn client(&self) -> tower_lsp::Client {
|
||||||
self.client.clone()
|
self.client.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fs(&self) -> crate::fs::FileManager {
|
||||||
|
self.fs.clone()
|
||||||
|
}
|
||||||
|
|
||||||
fn current_code_map(&self) -> DashMap<String, String> {
|
fn current_code_map(&self) -> DashMap<String, String> {
|
||||||
self.current_code_map.clone()
|
self.current_code_map.clone()
|
||||||
}
|
}
|
||||||
@ -125,15 +131,15 @@ impl Backend {
|
|||||||
let pos = params.doc.position;
|
let pos = params.doc.position;
|
||||||
let uri = params.doc.uri.to_string();
|
let uri = params.doc.uri.to_string();
|
||||||
let rope = ropey::Rope::from_str(¶ms.doc.source);
|
let rope = ropey::Rope::from_str(¶ms.doc.source);
|
||||||
let offset = crate::server::util::position_to_offset(pos, &rope).unwrap_or_default();
|
let offset = crate::lsp::util::position_to_offset(pos, &rope).unwrap_or_default();
|
||||||
|
|
||||||
Ok(DocParams {
|
Ok(DocParams {
|
||||||
uri: uri.to_string(),
|
uri: uri.to_string(),
|
||||||
pos,
|
pos,
|
||||||
language: params.doc.language_id.to_string(),
|
language: params.doc.language_id.to_string(),
|
||||||
prefix: crate::server::util::get_text_before(offset, &rope).unwrap_or_default(),
|
prefix: crate::lsp::util::get_text_before(offset, &rope).unwrap_or_default(),
|
||||||
suffix: crate::server::util::get_text_after(offset, &rope).unwrap_or_default(),
|
suffix: crate::lsp::util::get_text_after(offset, &rope).unwrap_or_default(),
|
||||||
line_before: crate::server::util::get_line_before(pos, &rope).unwrap_or_default(),
|
line_before: crate::lsp::util::get_line_before(pos, &rope).unwrap_or_default(),
|
||||||
rope,
|
rope,
|
||||||
})
|
})
|
||||||
}
|
}
|
@ -29,7 +29,7 @@ use tower_lsp::{
|
|||||||
Client, LanguageServer,
|
Client, LanguageServer,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{ast::types::VariableKind, executor::SourceRange, parser::PIPE_OPERATOR, server::backend::Backend as _};
|
use crate::{ast::types::VariableKind, executor::SourceRange, lsp::backend::Backend as _, parser::PIPE_OPERATOR};
|
||||||
|
|
||||||
/// A subcommand for running the server.
|
/// A subcommand for running the server.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@ -48,6 +48,8 @@ pub struct Server {
|
|||||||
pub struct Backend {
|
pub struct Backend {
|
||||||
/// The client for the backend.
|
/// The client for the backend.
|
||||||
pub client: Client,
|
pub client: Client,
|
||||||
|
/// The file system client to use.
|
||||||
|
pub fs: crate::fs::FileManager,
|
||||||
/// The stdlib completions for the language.
|
/// The stdlib completions for the language.
|
||||||
pub stdlib_completions: HashMap<String, CompletionItem>,
|
pub stdlib_completions: HashMap<String, CompletionItem>,
|
||||||
/// The stdlib signatures for the language.
|
/// The stdlib signatures for the language.
|
||||||
@ -70,11 +72,15 @@ pub struct Backend {
|
|||||||
|
|
||||||
// Implement the shared backend trait for the language server.
|
// Implement the shared backend trait for the language server.
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl crate::server::backend::Backend for Backend {
|
impl crate::lsp::backend::Backend for Backend {
|
||||||
fn client(&self) -> Client {
|
fn client(&self) -> Client {
|
||||||
self.client.clone()
|
self.client.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fs(&self) -> crate::fs::FileManager {
|
||||||
|
self.fs.clone()
|
||||||
|
}
|
||||||
|
|
||||||
fn current_code_map(&self) -> DashMap<String, String> {
|
fn current_code_map(&self) -> DashMap<String, String> {
|
||||||
self.current_code_map.clone()
|
self.current_code_map.clone()
|
||||||
}
|
}
|
@ -2,5 +2,5 @@
|
|||||||
|
|
||||||
mod backend;
|
mod backend;
|
||||||
pub mod copilot;
|
pub mod copilot;
|
||||||
pub mod lsp;
|
pub mod kcl;
|
||||||
mod util;
|
mod util;
|
@ -129,16 +129,22 @@ pub fn recast_wasm(json_str: &str) -> Result<JsValue, JsError> {
|
|||||||
pub struct ServerConfig {
|
pub struct ServerConfig {
|
||||||
into_server: js_sys::AsyncIterator,
|
into_server: js_sys::AsyncIterator,
|
||||||
from_server: web_sys::WritableStream,
|
from_server: web_sys::WritableStream,
|
||||||
|
fs: kcl_lib::fs::wasm::FileSystemManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
impl ServerConfig {
|
impl ServerConfig {
|
||||||
#[wasm_bindgen(constructor)]
|
#[wasm_bindgen(constructor)]
|
||||||
pub fn new(into_server: js_sys::AsyncIterator, from_server: web_sys::WritableStream) -> Self {
|
pub fn new(
|
||||||
|
into_server: js_sys::AsyncIterator,
|
||||||
|
from_server: web_sys::WritableStream,
|
||||||
|
fs: kcl_lib::fs::wasm::FileSystemManager,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
into_server,
|
into_server,
|
||||||
from_server,
|
from_server,
|
||||||
|
fs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -156,17 +162,19 @@ pub async fn kcl_lsp_run(config: ServerConfig) -> Result<(), JsValue> {
|
|||||||
let ServerConfig {
|
let ServerConfig {
|
||||||
into_server,
|
into_server,
|
||||||
from_server,
|
from_server,
|
||||||
|
fs,
|
||||||
} = config;
|
} = config;
|
||||||
|
|
||||||
let stdlib = kcl_lib::std::StdLib::new();
|
let stdlib = kcl_lib::std::StdLib::new();
|
||||||
let stdlib_completions = kcl_lib::server::lsp::get_completions_from_stdlib(&stdlib).map_err(|e| e.to_string())?;
|
let stdlib_completions = kcl_lib::lsp::kcl::get_completions_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())?;
|
let stdlib_signatures = kcl_lib::lsp::kcl::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| kcl_lib::server::lsp::Backend {
|
let (service, socket) = LspService::new(|client| kcl_lib::lsp::kcl::Backend {
|
||||||
client,
|
client,
|
||||||
|
fs: kcl_lib::fs::FileManager::new(fs),
|
||||||
stdlib_completions,
|
stdlib_completions,
|
||||||
stdlib_signatures,
|
stdlib_signatures,
|
||||||
token_types,
|
token_types,
|
||||||
@ -211,24 +219,24 @@ pub async fn copilot_lsp_run(config: ServerConfig, token: String) -> Result<(),
|
|||||||
let ServerConfig {
|
let ServerConfig {
|
||||||
into_server,
|
into_server,
|
||||||
from_server,
|
from_server,
|
||||||
|
fs,
|
||||||
} = config;
|
} = config;
|
||||||
|
|
||||||
let (service, socket) = LspService::build(|client| kcl_lib::server::copilot::Backend {
|
let (service, socket) = LspService::build(|client| kcl_lib::lsp::copilot::Backend {
|
||||||
client,
|
client,
|
||||||
|
fs: kcl_lib::fs::FileManager::new(fs),
|
||||||
current_code_map: Default::default(),
|
current_code_map: Default::default(),
|
||||||
editor_info: Arc::new(RwLock::new(
|
editor_info: Arc::new(RwLock::new(kcl_lib::lsp::copilot::types::CopilotEditorInfo::default())),
|
||||||
kcl_lib::server::copilot::types::CopilotEditorInfo::default(),
|
cache: kcl_lib::lsp::copilot::cache::CopilotCache::new(),
|
||||||
)),
|
|
||||||
cache: kcl_lib::server::copilot::cache::CopilotCache::new(),
|
|
||||||
token,
|
token,
|
||||||
})
|
})
|
||||||
.custom_method("setEditorInfo", kcl_lib::server::copilot::Backend::set_editor_info)
|
.custom_method("setEditorInfo", kcl_lib::lsp::copilot::Backend::set_editor_info)
|
||||||
.custom_method(
|
.custom_method(
|
||||||
"getCompletions",
|
"getCompletions",
|
||||||
kcl_lib::server::copilot::Backend::get_completions_cycling,
|
kcl_lib::lsp::copilot::Backend::get_completions_cycling,
|
||||||
)
|
)
|
||||||
.custom_method("notifyAccepted", kcl_lib::server::copilot::Backend::accept_completions)
|
.custom_method("notifyAccepted", kcl_lib::lsp::copilot::Backend::accept_completions)
|
||||||
.custom_method("notifyRejected", kcl_lib::server::copilot::Backend::reject_completions)
|
.custom_method("notifyRejected", kcl_lib::lsp::copilot::Backend::reject_completions)
|
||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
let input = wasm_bindgen_futures::stream::JsStream::from(into_server);
|
let input = wasm_bindgen_futures::stream::JsStream::from(into_server);
|
||||||
|
Reference in New Issue
Block a user