diff --git a/src/components/TextEditor.tsx b/src/components/TextEditor.tsx index e1891afed..da552dea1 100644 --- a/src/components/TextEditor.tsx +++ b/src/components/TextEditor.tsx @@ -3,9 +3,9 @@ import ReactCodeMirror, { ViewUpdate, keymap, } from '@uiw/react-codemirror' -import { FromServer, IntoServer } from 'editor/lsp/codec' -import Server from '../editor/lsp/server' -import Client from '../editor/lsp/client' +import { FromServer, IntoServer } from 'editor/plugins/lsp/codec' +import Server from '../editor/plugins/lsp/server' +import Client from '../editor/plugins/lsp/client' import { TEST } from 'env' import { useCommandsContext } from 'hooks/useCommandsContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext' @@ -15,8 +15,8 @@ import { useMemo, useRef } from 'react' import { linter, lintGutter } from '@codemirror/lint' import { useStore } from 'useStore' import { processCodeMirrorRanges } from 'lib/selections' -import { LanguageServerClient } from 'editor/lsp' -import kclLanguage from 'editor/lsp/language' +import { LanguageServerClient } from 'editor/plugins/lsp' +import kclLanguage from 'editor/plugins/lsp/kcl/language' import { EditorView, lineHighlightField } from 'editor/highlightextension' import { roundOff } from 'lib/utils' import { kclErrToDiagnostic } from 'lang/errors' @@ -27,7 +27,9 @@ import { engineCommandManager } from '../lang/std/engineConnection' import { kclManager, useKclContext } from 'lang/KclSingleton' import { ModelingMachineEvent } from 'machines/modelingMachine' 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 = { 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 = ({ theme, }: { @@ -91,7 +102,7 @@ export const TextEditor = ({ }) } - const lspClient = new LanguageServerClient({ client }) + const lspClient = new LanguageServerClient({ client, name: 'kcl' }) return { lspClient } }, [setIsKclLspServerReady]) @@ -107,7 +118,7 @@ export const TextEditor = ({ const lsp = kclLanguage({ // When we have more than one file, we'll need to change this. documentUri: `file:///we-just-have-one-file-for-now.kcl`, - workspaceFolders: null, + workspaceFolders: getWorkspaceFolders(), client: kclLspClient, }) @@ -128,7 +139,7 @@ export const TextEditor = ({ }) } - const lspClient = new LanguageServerClient({ client }) + const lspClient = new LanguageServerClient({ client, name: 'copilot' }) return { lspClient } }, [setIsCopilotLspServerReady]) @@ -141,10 +152,10 @@ export const TextEditor = ({ let plugin = null if (isCopilotLspServerReady && !TEST) { // Set up the lsp plugin. - const lsp = copilotBundle({ + const lsp = copilotPlugin({ // When we have more than one file, we'll need to change this. documentUri: `file:///we-just-have-one-file-for-now.kcl`, - workspaceFolders: null, + workspaceFolders: getWorkspaceFolders(), client: copilotLspClient, allowHTMLContent: true, }) diff --git a/src/editor/lsp/client.ts b/src/editor/plugins/lsp/client.ts similarity index 95% rename from src/editor/lsp/client.ts rename to src/editor/plugins/lsp/client.ts index 075126787..9b542faac 100644 --- a/src/editor/lsp/client.ts +++ b/src/editor/plugins/lsp/client.ts @@ -65,6 +65,7 @@ export default class Client extends jsrpc.JSONRPCServerAndClient { afterInitializedHooks: (() => Promise)[] = [] #fromServer: FromServer private serverCapabilities: LSP.ServerCapabilities = {} + private notifyFn: ((message: LSP.NotificationMessage) => void) | null = null constructor(fromServer: FromServer, intoServer: IntoServer) { super( @@ -167,9 +168,15 @@ export default class Client extends jsrpc.JSONRPCServerAndClient { return this.serverCapabilities } + setNotifyFn(fn: (message: LSP.NotificationMessage) => void): void { + this.notifyFn = fn + } + async processNotifications(): Promise { for await (const notification of this.#fromServer.notifications) { - await this.receiveAndSend(notification) + if (this.notifyFn) { + this.notifyFn(notification) + } } } diff --git a/src/editor/lsp/codec.ts b/src/editor/plugins/lsp/codec.ts similarity index 100% rename from src/editor/lsp/codec.ts rename to src/editor/plugins/lsp/codec.ts diff --git a/src/editor/lsp/codec/bytes.ts b/src/editor/plugins/lsp/codec/bytes.ts similarity index 100% rename from src/editor/lsp/codec/bytes.ts rename to src/editor/plugins/lsp/codec/bytes.ts diff --git a/src/editor/lsp/codec/demuxer.ts b/src/editor/plugins/lsp/codec/demuxer.ts similarity index 100% rename from src/editor/lsp/codec/demuxer.ts rename to src/editor/plugins/lsp/codec/demuxer.ts diff --git a/src/editor/lsp/codec/headers.ts b/src/editor/plugins/lsp/codec/headers.ts similarity index 100% rename from src/editor/lsp/codec/headers.ts rename to src/editor/plugins/lsp/codec/headers.ts diff --git a/src/editor/lsp/codec/map.ts b/src/editor/plugins/lsp/codec/map.ts similarity index 100% rename from src/editor/lsp/codec/map.ts rename to src/editor/plugins/lsp/codec/map.ts diff --git a/src/editor/lsp/codec/queue.ts b/src/editor/plugins/lsp/codec/queue.ts similarity index 100% rename from src/editor/lsp/codec/queue.ts rename to src/editor/plugins/lsp/codec/queue.ts diff --git a/src/editor/copilot/index.ts b/src/editor/plugins/lsp/copilot/index.ts similarity index 90% rename from src/editor/copilot/index.ts rename to src/editor/plugins/lsp/copilot/index.ts index 6046e6ce2..c181bc812 100644 --- a/src/editor/copilot/index.ts +++ b/src/editor/plugins/lsp/copilot/index.ts @@ -11,30 +11,20 @@ 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({ - combine(value: readonly string[]) { - return value[value.length - 1] - }, -}) - -export const relDocPath = Facet.define({ - combine(value: readonly string[]) { - return value[value.length - 1] - }, -}) +import { offsetToPos, posToOffset } from 'editor/plugins/lsp/util' +import { LanguageServerOptions, LanguageServerClient } from 'editor/plugins/lsp' +import { + LanguageServerPlugin, + documentUri, + languageId, + workspaceFolders, +} from 'editor/plugins/lsp/plugin' const ghostMark = Decoration.mark({ class: 'cm-ghostText' }) @@ -361,9 +351,9 @@ const completionRequester = (client: LanguageServerClient) => { const pos = state.selection.main.head const source = state.doc.toString() - const path = state.facet(docPath) - const relativePath = state.facet(relDocPath) - const languageId = 'kcl' + const dUri = state.facet(documentUri) + const path = dUri.split('/').pop()! + const relativePath = dUri.replace('file://', '') // Set a new timeout to request completion timeout = setTimeout(async () => { @@ -378,9 +368,9 @@ const completionRequester = (client: LanguageServerClient) => { indentSize: 1, insertSpaces: true, path, - uri: `file://${path}`, + uri: dUri, relativePath, - languageId, + languageId: state.facet(languageId), position: offsetToPos(state.doc, pos), }, }) @@ -483,21 +473,24 @@ const completionRequester = (client: LanguageServerClient) => { }) } -export function copilotServer(options: LanguageServerOptions) { - let plugin: LanguageServerPlugin - return ViewPlugin.define( - (view) => - (plugin = new LanguageServerPlugin(view, options.allowHTMLContent)) - ) -} +export const copilotPlugin = (options: LanguageServerOptions): Extension => { + let plugin: LanguageServerPlugin | null = null -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), -] + return [ + documentUri.of(options.documentUri), + languageId.of('kcl'), + workspaceFolders.of(options.workspaceFolders), + ViewPlugin.define( + (view) => + (plugin = new LanguageServerPlugin( + options.client, + view, + options.allowHTMLContent + )) + ), + completionDecoration, + Prec.highest(completionPlugin(options.client)), + Prec.highest(viewCompletionPlugin(options.client)), + completionRequester(options.client), + ] +} diff --git a/src/editor/lsp/index.ts b/src/editor/plugins/lsp/index.ts similarity index 84% rename from src/editor/lsp/index.ts rename to src/editor/plugins/lsp/index.ts index 559b692f0..fa5267462 100644 --- a/src/editor/lsp/index.ts +++ b/src/editor/plugins/lsp/index.ts @@ -1,7 +1,7 @@ import type * as LSP from 'vscode-languageserver-protocol' import Client from './client' -import { LanguageServerPlugin } from './plugin' -import { SemanticToken, deserializeTokens } from './semantic_tokens' +import { SemanticToken, deserializeTokens } from './kcl/semantic_tokens' +import { LanguageServerPlugin } from 'editor/plugins/lsp/plugin' export interface CopilotGetCompletionsParams { doc: { @@ -90,26 +90,22 @@ interface LSPNotifyMap { '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 { 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 { private client: Client + private name: string public ready: boolean @@ -124,6 +120,7 @@ export class LanguageServerClient { constructor(options: LanguageServerClientOptions) { this.plugins = [] this.client = options.client + this.name = options.name this.ready = false @@ -133,11 +130,16 @@ export class LanguageServerClient { async initialize() { // Start the client in the background. + this.client.setNotifyFn(this.processNotifications.bind(this)) this.client.start() this.ready = true } + getName(): string { + return this.name + } + getServerCapabilities(): LSP.ServerCapabilities { return this.client.getServerCapabilities() } @@ -156,6 +158,11 @@ export class LanguageServerClient { } async updateSemanticTokens(uri: string) { + const serverCapabilities = this.getServerCapabilities() + if (!serverCapabilities.semanticTokensProvider) { + return + } + // Make sure we can only run, if we aren't already running. if (!this.isUpdatingSemanticTokens) { this.isUpdatingSemanticTokens = true @@ -180,10 +187,18 @@ export class LanguageServerClient { } async textDocumentHover(params: LSP.HoverParams) { + const serverCapabilities = this.getServerCapabilities() + if (!serverCapabilities.hoverProvider) { + return + } return await this.request('textDocument/hover', params) } async textDocumentCompletion(params: LSP.CompletionParams) { + const serverCapabilities = this.getServerCapabilities() + if (!serverCapabilities.completionProvider) { + return + } return await this.request('textDocument/completion', params) } @@ -234,11 +249,12 @@ export class LanguageServerClient { 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 processNotifications(notification: LSP.NotificationMessage) { for (const plugin of this.plugins) plugin.processNotification(notification) } } diff --git a/src/editor/plugins/lsp/kcl/index.ts b/src/editor/plugins/lsp/kcl/index.ts new file mode 100644 index 000000000..91cb54116 --- /dev/null +++ b/src/editor/plugins/lsp/kcl/index.ts @@ -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, + } + ) + }, + ], + }), + ] +} diff --git a/src/editor/lsp/language.ts b/src/editor/plugins/lsp/kcl/language.ts similarity index 90% rename from src/editor/lsp/language.ts rename to src/editor/plugins/lsp/kcl/language.ts index 7291f1a7c..89ef7d84e 100644 --- a/src/editor/lsp/language.ts +++ b/src/editor/plugins/lsp/kcl/language.ts @@ -5,8 +5,8 @@ import { defineLanguageFacet, LanguageSupport, } from '@codemirror/language' -import { LanguageServerClient } from '.' -import { kclPlugin } from './plugin' +import { LanguageServerClient } from 'editor/plugins/lsp' +import { kclPlugin } from '.' import type * as LSP from 'vscode-languageserver-protocol' import { parser as jsParser } from '@lezer/javascript' import { EditorState } from '@uiw/react-codemirror' @@ -14,7 +14,7 @@ import { EditorState } from '@uiw/react-codemirror' const data = defineLanguageFacet({}) export interface LanguageOptions { - workspaceFolders: LSP.WorkspaceFolder[] | null + workspaceFolders: LSP.WorkspaceFolder[] documentUri: string client: LanguageServerClient } diff --git a/src/editor/lsp/parser.ts b/src/editor/plugins/lsp/kcl/parser.ts similarity index 97% rename from src/editor/lsp/parser.ts rename to src/editor/plugins/lsp/kcl/parser.ts index c01389d9d..50a5daf4a 100644 --- a/src/editor/lsp/parser.ts +++ b/src/editor/plugins/lsp/kcl/parser.ts @@ -9,8 +9,8 @@ import { NodeType, NodeSet, } from '@lezer/common' -import { LanguageServerClient } from '.' -import { posToOffset } from 'editor/lsp/util' +import { LanguageServerClient } from 'editor/plugins/lsp' +import { posToOffset } from 'editor/plugins/lsp/util' import { SemanticToken } from './semantic_tokens' import { DocInput } from '@codemirror/language' import { tags, styleTags } from '@lezer/highlight' diff --git a/src/editor/lsp/semantic_tokens.ts b/src/editor/plugins/lsp/kcl/semantic_tokens.ts similarity index 100% rename from src/editor/lsp/semantic_tokens.ts rename to src/editor/plugins/lsp/kcl/semantic_tokens.ts diff --git a/src/editor/lsp/plugin.ts b/src/editor/plugins/lsp/plugin.ts similarity index 74% rename from src/editor/lsp/plugin.ts rename to src/editor/plugins/lsp/plugin.ts index 5281b8202..64341c9c8 100644 --- a/src/editor/lsp/plugin.ts +++ b/src/editor/plugins/lsp/plugin.ts @@ -1,13 +1,7 @@ -import { autocompletion, completeFromList } from '@codemirror/autocomplete' +import { completeFromList } from '@codemirror/autocomplete' import { setDiagnostics } from '@codemirror/lint' import { Facet } from '@codemirror/state' -import { - EditorView, - ViewPlugin, - Tooltip, - hoverTooltip, - tooltips, -} from '@codemirror/view' +import { EditorView, Tooltip } from '@codemirror/view' import { DiagnosticSeverity, CompletionItemKind, @@ -23,9 +17,17 @@ import type { import type { PublishDiagnosticsParams } from 'vscode-languageserver-protocol' import type { ViewUpdate, PluginValue } from '@codemirror/view' 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 { 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({ combine: useLast }) +export const languageId = Facet.define({ combine: useLast }) +export const workspaceFolders = Facet.define< + LSP.WorkspaceFolder[], + LSP.WorkspaceFolder[] +>({ combine: useLast }) const changesDelay = 500 @@ -33,31 +35,22 @@ const CompletionItemKindMap = Object.fromEntries( Object.entries(CompletionItemKind).map(([key, value]) => [value, key]) ) as Record -const useLast = (values: readonly any[]) => values.reduce((_, v) => v, '') -const documentUri = Facet.define({ combine: useLast }) -const languageId = Facet.define({ combine: useLast }) -const client = Facet.define({ - combine: useLast, -}) - -export interface LanguageServerOptions { - workspaceFolders: LSP.WorkspaceFolder[] | null - documentUri: string - allowHTMLContent: boolean - client: LanguageServerClient -} - export class LanguageServerPlugin implements PluginValue { public client: LanguageServerClient - private documentUri: string private languageId: string + private workspaceFolders: LSP.WorkspaceFolder[] private documentVersion: number - constructor(private view: EditorView, private allowHTMLContent: boolean) { - this.client = this.view.state.facet(client) + constructor( + client: LanguageServerClient, + private view: EditorView, + private allowHTMLContent: boolean + ) { + this.client = client this.documentUri = this.view.state.facet(documentUri) this.languageId = this.view.state.facet(languageId) + this.workspaceFolders = this.view.state.facet(workspaceFolders) this.documentVersion = 0 this.client.attachPlugin(this) @@ -238,11 +231,28 @@ export class LanguageServerPlugin implements PluginValue { return completeFromList(options)(context) } - processNotification(notification: Notification) { + processNotification(notification: LSP.NotificationMessage) { try { switch (notification.method) { 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) { 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( contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[] ): string { diff --git a/src/editor/lsp/server-capability-registration.ts b/src/editor/plugins/lsp/server-capability-registration.ts similarity index 95% rename from src/editor/lsp/server-capability-registration.ts rename to src/editor/plugins/lsp/server-capability-registration.ts index 780e77edb..a31370c9e 100644 --- a/src/editor/lsp/server-capability-registration.ts +++ b/src/editor/plugins/lsp/server-capability-registration.ts @@ -34,6 +34,8 @@ const ServerCapabilitiesProviders: IMethodServerCapabilityProviderDictionary = { 'textDocument/foldingRange': 'foldingRangeProvider', 'textDocument/declaration': 'declarationProvider', 'textDocument/executeCommand': 'executeCommandProvider', + 'textDocument/semanticTokens/full': 'semanticTokensProvider', + 'textDocument/publishDiagnostics': 'diagnosticsProvider', } function registerServerCapability( diff --git a/src/editor/lsp/server.ts b/src/editor/plugins/lsp/server.ts similarity index 83% rename from src/editor/lsp/server.ts rename to src/editor/plugins/lsp/server.ts index 457d0df7c..cf550e92a 100644 --- a/src/editor/lsp/server.ts +++ b/src/editor/plugins/lsp/server.ts @@ -3,8 +3,9 @@ import init, { InitOutput, kcl_lsp_run, ServerConfig, -} from '../../wasm-lib/pkg/wasm_lib' +} from 'wasm-lib/pkg/wasm_lib' import { FromServer, IntoServer } from './codec' +import { fileSystemManager } from 'lang/std/fileSystemManager' export default class Server { readonly initOutput: InitOutput @@ -31,7 +32,11 @@ export default class Server { } async start(type_: 'kcl' | 'copilot', token?: string): Promise { - const config = new ServerConfig(this.#intoServer, this.#fromServer) + const config = new ServerConfig( + this.#intoServer, + this.#fromServer, + fileSystemManager + ) if (type_ === 'copilot') { if (!token) { throw new Error('auth token is required for copilot') diff --git a/src/editor/lsp/tracer.ts b/src/editor/plugins/lsp/tracer.ts similarity index 100% rename from src/editor/lsp/tracer.ts rename to src/editor/plugins/lsp/tracer.ts diff --git a/src/editor/lsp/util.ts b/src/editor/plugins/lsp/util.ts similarity index 72% rename from src/editor/lsp/util.ts rename to src/editor/plugins/lsp/util.ts index 38ae1c23e..7726597cf 100644 --- a/src/editor/lsp/util.ts +++ b/src/editor/plugins/lsp/util.ts @@ -1,4 +1,4 @@ -import { Facet, Text } from '@codemirror/state' +import { Text } from '@codemirror/state' export function posToOffset( doc: Text, @@ -17,7 +17,3 @@ export function offsetToPos(doc: Text, offset: number) { character: offset - line.from, } } - -export const docPathFacet = Facet.define({ - combine: (values) => values[values.length - 1], -}) diff --git a/src/lang/std/fileSystemManager.ts b/src/lang/std/fileSystemManager.ts index defeba8e8..3805c9539 100644 --- a/src/lang/std/fileSystemManager.ts +++ b/src/lang/std/fileSystemManager.ts @@ -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 { join } from '@tauri-apps/api/path' @@ -53,6 +57,30 @@ class FileSystemManager { return tauriExists(file) }) } + + getAllFiles(path: string): Promise { + // 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() diff --git a/src/machines/authMachine.ts b/src/machines/authMachine.ts index 97b3a8627..5a9433fe9 100644 --- a/src/machines/authMachine.ts +++ b/src/machines/authMachine.ts @@ -37,7 +37,10 @@ export type Events = } 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( { @@ -135,3 +138,23 @@ async function getUser(context: UserContext) { 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 '' +} diff --git a/src/wasm-lib/kcl/src/fs/local.rs b/src/wasm-lib/kcl/src/fs/local.rs index f563859a3..7bfb53f89 100644 --- a/src/wasm-lib/kcl/src/fs/local.rs +++ b/src/wasm-lib/kcl/src/fs/local.rs @@ -53,4 +53,38 @@ impl FileSystem for FileManager { } }) } + + async fn get_all_files>( + &self, + path: P, + source_range: crate::executor::SourceRange, + ) -> Result, 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) + } } diff --git a/src/wasm-lib/kcl/src/fs/mod.rs b/src/wasm-lib/kcl/src/fs/mod.rs index b321bb523..5cdb9e740 100644 --- a/src/wasm-lib/kcl/src/fs/mod.rs +++ b/src/wasm-lib/kcl/src/fs/mod.rs @@ -28,4 +28,11 @@ pub trait FileSystem: Clone { path: P, source_range: crate::executor::SourceRange, ) -> Result; + + /// Get all the files in a directory recursively. + async fn get_all_files>( + &self, + path: P, + source_range: crate::executor::SourceRange, + ) -> Result, crate::errors::KclError>; } diff --git a/src/wasm-lib/kcl/src/fs/wasm.rs b/src/wasm-lib/kcl/src/fs/wasm.rs index 9ad6ec866..16e872d31 100644 --- a/src/wasm-lib/kcl/src/fs/wasm.rs +++ b/src/wasm-lib/kcl/src/fs/wasm.rs @@ -18,6 +18,9 @@ extern "C" { #[wasm_bindgen(method, js_name = exists, catch)] fn exists(this: &FileSystemManager, path: String) -> Result; + + #[wasm_bindgen(method, js_name = getAllFiles, catch)] + fn get_all_files(this: &FileSystemManager, path: String) -> Result; } #[derive(Debug, Clone)] @@ -31,6 +34,9 @@ impl FileManager { } } +unsafe impl Send for FileManager {} +unsafe impl Sync for FileManager {} + #[async_trait::async_trait(?Send)] impl FileSystem for FileManager { async fn read>( @@ -112,4 +118,53 @@ impl FileSystem for FileManager { Ok(it_exists) } + + async fn get_all_files>( + &self, + path: P, + source_range: crate::executor::SourceRange, + ) -> Result, 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 = 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()) + } } diff --git a/src/wasm-lib/kcl/src/lib.rs b/src/wasm-lib/kcl/src/lib.rs index a1101ebf2..92ff9bb1f 100644 --- a/src/wasm-lib/kcl/src/lib.rs +++ b/src/wasm-lib/kcl/src/lib.rs @@ -10,7 +10,7 @@ pub mod engine; pub mod errors; pub mod executor; pub mod fs; +pub mod lsp; pub mod parser; -pub mod server; pub mod std; pub mod token; diff --git a/src/wasm-lib/kcl/src/server/backend.rs b/src/wasm-lib/kcl/src/lsp/backend.rs similarity index 98% rename from src/wasm-lib/kcl/src/server/backend.rs rename to src/wasm-lib/kcl/src/lsp/backend.rs index 601933ee2..78d104b0f 100644 --- a/src/wasm-lib/kcl/src/server/backend.rs +++ b/src/wasm-lib/kcl/src/lsp/backend.rs @@ -13,6 +13,8 @@ use tower_lsp::lsp_types::{ pub trait Backend { fn client(&self) -> tower_lsp::Client; + fn fs(&self) -> crate::fs::FileManager; + /// Get the current code map. fn current_code_map(&self) -> DashMap; diff --git a/src/wasm-lib/kcl/src/server/copilot/cache.rs b/src/wasm-lib/kcl/src/lsp/copilot/cache.rs similarity index 98% rename from src/wasm-lib/kcl/src/server/copilot/cache.rs rename to src/wasm-lib/kcl/src/lsp/copilot/cache.rs index a52b93b13..ad7fce8dd 100644 --- a/src/wasm-lib/kcl/src/server/copilot/cache.rs +++ b/src/wasm-lib/kcl/src/lsp/copilot/cache.rs @@ -6,7 +6,7 @@ use std::{ sync::{Mutex, RwLock}, }; -use crate::server::copilot::types::CopilotCompletionResponse; +use crate::lsp::copilot::types::CopilotCompletionResponse; // if file changes, keep the cache. // if line number is different for an existing file, clean. diff --git a/src/wasm-lib/kcl/src/server/copilot/mod.rs b/src/wasm-lib/kcl/src/lsp/copilot/mod.rs similarity index 94% rename from src/wasm-lib/kcl/src/server/copilot/mod.rs rename to src/wasm-lib/kcl/src/lsp/copilot/mod.rs index 7949fac76..62d836e7c 100644 --- a/src/wasm-lib/kcl/src/server/copilot/mod.rs +++ b/src/wasm-lib/kcl/src/lsp/copilot/mod.rs @@ -23,7 +23,7 @@ use tower_lsp::{ LanguageServer, }; -use crate::server::{ +use crate::lsp::{ backend::Backend as _, copilot::types::{CopilotCompletionResponse, CopilotEditorInfo, CopilotLspCompletionParams, DocParams}, }; @@ -42,6 +42,8 @@ impl Success { pub struct Backend { /// The client is used to send notifications and requests to the client. pub client: tower_lsp::Client, + /// The file system client to use. + pub fs: crate::fs::FileManager, /// Current code. pub current_code_map: DashMap, /// 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. #[async_trait::async_trait] -impl crate::server::backend::Backend for Backend { +impl crate::lsp::backend::Backend for Backend { fn client(&self) -> tower_lsp::Client { self.client.clone() } + fn fs(&self) -> crate::fs::FileManager { + self.fs.clone() + } + fn current_code_map(&self) -> DashMap { self.current_code_map.clone() } @@ -125,15 +131,15 @@ impl Backend { 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(); + let offset = crate::lsp::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(), + prefix: crate::lsp::util::get_text_before(offset, &rope).unwrap_or_default(), + suffix: crate::lsp::util::get_text_after(offset, &rope).unwrap_or_default(), + line_before: crate::lsp::util::get_line_before(pos, &rope).unwrap_or_default(), rope, }) } diff --git a/src/wasm-lib/kcl/src/server/copilot/types.rs b/src/wasm-lib/kcl/src/lsp/copilot/types.rs similarity index 100% rename from src/wasm-lib/kcl/src/server/copilot/types.rs rename to src/wasm-lib/kcl/src/lsp/copilot/types.rs diff --git a/src/wasm-lib/kcl/src/server/lsp/mod.rs b/src/wasm-lib/kcl/src/lsp/kcl/mod.rs similarity index 98% rename from src/wasm-lib/kcl/src/server/lsp/mod.rs rename to src/wasm-lib/kcl/src/lsp/kcl/mod.rs index 29b8f95b2..c8e4e939b 100644 --- a/src/wasm-lib/kcl/src/server/lsp/mod.rs +++ b/src/wasm-lib/kcl/src/lsp/kcl/mod.rs @@ -29,7 +29,7 @@ use tower_lsp::{ 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. #[derive(Clone, Debug)] @@ -48,6 +48,8 @@ pub struct Server { pub struct Backend { /// The client for the backend. pub client: Client, + /// The file system client to use. + pub fs: crate::fs::FileManager, /// The stdlib completions for the language. pub stdlib_completions: HashMap, /// The stdlib signatures for the language. @@ -70,11 +72,15 @@ pub struct Backend { // Implement the shared backend trait for the language server. #[async_trait::async_trait] -impl crate::server::backend::Backend for Backend { +impl crate::lsp::backend::Backend for Backend { fn client(&self) -> Client { self.client.clone() } + fn fs(&self) -> crate::fs::FileManager { + self.fs.clone() + } + fn current_code_map(&self) -> DashMap { self.current_code_map.clone() } diff --git a/src/wasm-lib/kcl/src/server/mod.rs b/src/wasm-lib/kcl/src/lsp/mod.rs similarity index 86% rename from src/wasm-lib/kcl/src/server/mod.rs rename to src/wasm-lib/kcl/src/lsp/mod.rs index 32f5de89d..4069f036d 100644 --- a/src/wasm-lib/kcl/src/server/mod.rs +++ b/src/wasm-lib/kcl/src/lsp/mod.rs @@ -2,5 +2,5 @@ mod backend; pub mod copilot; -pub mod lsp; +pub mod kcl; mod util; diff --git a/src/wasm-lib/kcl/src/server/util.rs b/src/wasm-lib/kcl/src/lsp/util.rs similarity index 100% rename from src/wasm-lib/kcl/src/server/util.rs rename to src/wasm-lib/kcl/src/lsp/util.rs diff --git a/src/wasm-lib/src/lib.rs b/src/wasm-lib/src/lib.rs index 8f793c9aa..92c8018ab 100644 --- a/src/wasm-lib/src/lib.rs +++ b/src/wasm-lib/src/lib.rs @@ -129,16 +129,22 @@ pub fn recast_wasm(json_str: &str) -> Result { pub struct ServerConfig { into_server: js_sys::AsyncIterator, from_server: web_sys::WritableStream, + fs: kcl_lib::fs::wasm::FileSystemManager, } #[cfg(target_arch = "wasm32")] #[wasm_bindgen] impl ServerConfig { #[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 { into_server, from_server, + fs, } } } @@ -156,17 +162,19 @@ pub async fn kcl_lsp_run(config: ServerConfig) -> Result<(), JsValue> { let ServerConfig { into_server, from_server, + fs, } = config; 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_signatures = kcl_lib::server::lsp::get_signatures_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::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 have a test for it. 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, + fs: kcl_lib::fs::FileManager::new(fs), stdlib_completions, stdlib_signatures, token_types, @@ -211,24 +219,24 @@ pub async fn copilot_lsp_run(config: ServerConfig, token: String) -> Result<(), let ServerConfig { into_server, from_server, + fs, } = config; - let (service, socket) = LspService::build(|client| kcl_lib::server::copilot::Backend { + let (service, socket) = LspService::build(|client| kcl_lib::lsp::copilot::Backend { client, + fs: kcl_lib::fs::FileManager::new(fs), 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(), + editor_info: Arc::new(RwLock::new(kcl_lib::lsp::copilot::types::CopilotEditorInfo::default())), + cache: kcl_lib::lsp::copilot::cache::CopilotCache::new(), 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( "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("notifyRejected", kcl_lib::server::copilot::Backend::reject_completions) + .custom_method("notifyAccepted", kcl_lib::lsp::copilot::Backend::accept_completions) + .custom_method("notifyRejected", kcl_lib::lsp::copilot::Backend::reject_completions) .finish(); let input = wasm_bindgen_futures::stream::JsStream::from(into_server);