diff --git a/client/src/extension.ts b/client/src/extension.ts index 251309c..aafcff7 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -4,8 +4,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. * ------------------------------------------------------------------------------------------ */ -import * as path from 'path'; -// import { workspace, ExtensionContext } from "vscode"; import * as vscode from 'vscode'; import * as which from 'which'; @@ -13,68 +11,46 @@ import { LanguageClient, LanguageClientOptions, ServerOptions, - TransportKind, } from 'vscode-languageclient/node'; let client: LanguageClient; +function findNushellExecutable(): string | null { + try { + // Get the configured executable path from VSCode settings + // Use null for resource to get global/workspace settings + const config = vscode.workspace.getConfiguration('nushellLanguageServer', null); + const configuredPath = config.get('nushellExecutablePath', 'nu'); + + // If user configured a specific path, try to find it + if (configuredPath && configuredPath !== 'nu') { + // User specified a custom path + try { + // Test if the configured path works + return which.sync(configuredPath, { nothrow: true }); + } catch { + // Fall back to searching PATH for 'nu' + } + } + + // Fall back to searching PATH for 'nu' + return which.sync('nu', { nothrow: true }); + } catch (error) { + return null; + } +} + export function activate(context: vscode.ExtensionContext) { console.log('Terminals: ' + (vscode.window).terminals.length); + + // Find Nushell executable once and reuse it + const found_nushell_path = findNushellExecutable(); + context.subscriptions.push( vscode.window.registerTerminalProfileProvider('nushell_default', { provideTerminalProfile( token: vscode.CancellationToken, ): vscode.ProviderResult { - // const which = require('which'); - // const path = require('path'); - - const PATH_FROM_ENV = process.env['PATH']; - const pathsToCheck = [ - PATH_FROM_ENV, - // cargo install location - (process.env['CARGO_HOME'] || '~/.cargo') + '/bin', - - // winget on Windows install location - 'c:\\program files\\nu\\bin', - // just add a few other drives for fun - 'd:\\program files\\nu\\bin', - 'e:\\program files\\nu\\bin', - 'f:\\program files\\nu\\bin', - - // SCOOP:TODO - // all user installed programs and scoop itself install to - // c:\users\\scoop\ unless SCOOP env var is set - // globally installed programs go in - // c:\programdata\scoop unless SCOOP_GLOBAL env var is set - // scoop install location - // SCOOP should already set up the correct `PATH` env var - //"~/scoop/apps/nu/*/nu.exe", - //"~/scoop/shims/nu.exe", - - // chocolatey install location - same as winget - // 'c:\\program files\\nu\\bin\\nu.exe', - - // macos dmg install - // we currentl don't have a dmg install - - // linux and mac zips can be put anywhere so it's hard to guess - - // brew install location mac - // intel - '/usr/local/bin', - // arm - '/opt/homebrew/bin', - - // native package manager install location - // standard location should be in `PATH` env var - //"/usr/bin/nu", - ]; - - const found_nushell_path = which.sync('nu', { - nothrow: true, - path: pathsToCheck.join(path.delimiter), - }); - if (found_nushell_path == null) { console.log( 'Nushell not found in env:PATH or any of the heuristic locations.', @@ -116,37 +92,39 @@ export function activate(context: vscode.ExtensionContext) { }), ); - // The server is implemented in node - const serverModule = context.asAbsolutePath( - path.join('out', 'server', 'src', 'server.js'), - ); - - // The debug options for the server - // --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging - const debugOptions = { execArgv: ['--nolazy', '--inspect=6009'] }; + // Check if Nushell was found for LSP server + if (!found_nushell_path) { + vscode.window.showErrorMessage( + 'Nushell executable not found. Please install Nushell and restart VSCode.', + 'Install from website' + ).then((selection) => { + if (selection) { + vscode.env.openExternal(vscode.Uri.parse('https://www.nushell.sh/')); + } + }); + return; + } - // If the extension is launched in debug mode then the debug server options are used - // Otherwise the run options are used + // Use Nushell's native LSP server const serverOptions: ServerOptions = { - run: { module: serverModule, transport: TransportKind.ipc }, - debug: { - module: serverModule, - transport: TransportKind.ipc, - options: debugOptions, + run: { + command: found_nushell_path, + args: ['--lsp'] + }, + debug: { + command: found_nushell_path, + args: ['--lsp'] }, }; // Options to control the language client const clientOptions: LanguageClientOptions = { - // Register the server for plain text documents + // Register the server for nushell files documentSelector: [{ scheme: 'file', language: 'nushell' }], synchronize: { - // Notify the server about file changes to '.clientrc files contained in the workspace - fileEvents: vscode.workspace.createFileSystemWatcher('**/.clientrc'), - }, - markdown: { - isTrusted: true, - }, + // Notify the server about file changes to nushell files + fileEvents: vscode.workspace.createFileSystemWatcher('**/*.nu'), + } }; // Create the language client and start the client. @@ -158,7 +136,9 @@ export function activate(context: vscode.ExtensionContext) { ); // Start the client. This will also launch the server - client.start(); + client.start().catch((error) => { + vscode.window.showErrorMessage(`Failed to start Nushell language server: ${error.message}`); + }); } export function deactivate(): Thenable | undefined { diff --git a/package.json b/package.json index 5b0aaf2..f85313a 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "activationEvents": [ "onTerminalProfile:nushell_default" ], - "main": "out/client/src/extension.js", + "main": "out/extension.js", "contributes": { "languages": [ { @@ -143,12 +143,12 @@ }, "scripts": { "vscode:prepublish": "npm run lint && npm run compile", - "esbuild-base": "npx esbuild server/src/server.ts client/src/extension.ts --bundle --outdir=out --external:vscode --format=cjs --platform=node", + "esbuild-base": "npx esbuild client/src/extension.ts --bundle --outdir=out --external:vscode --format=cjs --platform=node", "esbuild": "npm run esbuild-base -- --sourcemap --minify", "compile": "npm run esbuild", - "install": "cd server && npm install && cd ../client && npm install && cd ..", + "install": "cd client && npm install && cd ..", "watch": "npm run esbuild-base -- --sourcemap --watch", - "lint": "npx eslint ./client/src/extension.ts ./server/src/server.ts", + "lint": "npx eslint ./client/src/extension.ts", "test": "sh ./scripts/e2e.sh", "test:grammar": "vscode-tmgrammar-snap tests/cases/*", "fmt": "prettier . --write" @@ -156,7 +156,6 @@ "dependencies": { "glob": "11.0.0", "nushell-lsp-client": "file:client", - "nushell-lsp-server": "file:server", "os": "0.1.2" }, "devDependencies": { diff --git a/server/package-lock.json b/server/package-lock.json deleted file mode 100644 index 1b58d48..0000000 --- a/server/package-lock.json +++ /dev/null @@ -1,152 +0,0 @@ -{ - "name": "nushell-lsp-server", - "version": "1.2.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "nushell-lsp-server", - "version": "1.2.0", - "license": "MIT", - "dependencies": { - "@types/vscode": "1.93.0", - "tmp": "0.2.3", - "vscode-languageserver": "9.0.1", - "vscode-languageserver-textdocument": "1.0.12", - "vscode-uri": "3.0.8" - }, - "devDependencies": { - "@types/tmp": "0.2.6" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@types/tmp": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.6.tgz", - "integrity": "sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/vscode": { - "version": "1.93.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.93.0.tgz", - "integrity": "sha512-kUK6jAHSR5zY8ps42xuW89NLcBpw1kOabah7yv38J8MyiYuOHxLQBi0e7zeXbQgVefDy/mZZetqEFC+Fl5eIEQ==", - "license": "MIT" - }, - "node_modules/tmp": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", - "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, - "node_modules/vscode-jsonrpc": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", - "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/vscode-languageserver": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", - "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", - "license": "MIT", - "dependencies": { - "vscode-languageserver-protocol": "3.17.5" - }, - "bin": { - "installServerIntoExtension": "bin/installServerIntoExtension" - } - }, - "node_modules/vscode-languageserver-protocol": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", - "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", - "license": "MIT", - "dependencies": { - "vscode-jsonrpc": "8.2.0", - "vscode-languageserver-types": "3.17.5" - } - }, - "node_modules/vscode-languageserver-textdocument": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", - "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", - "license": "MIT" - }, - "node_modules/vscode-languageserver-types": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", - "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", - "license": "MIT" - }, - "node_modules/vscode-uri": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", - "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", - "license": "MIT" - } - }, - "dependencies": { - "@types/tmp": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.6.tgz", - "integrity": "sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==", - "dev": true - }, - "@types/vscode": { - "version": "1.93.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.93.0.tgz", - "integrity": "sha512-kUK6jAHSR5zY8ps42xuW89NLcBpw1kOabah7yv38J8MyiYuOHxLQBi0e7zeXbQgVefDy/mZZetqEFC+Fl5eIEQ==" - }, - "tmp": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", - "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==" - }, - "vscode-jsonrpc": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", - "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==" - }, - "vscode-languageserver": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", - "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", - "requires": { - "vscode-languageserver-protocol": "3.17.5" - } - }, - "vscode-languageserver-protocol": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", - "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", - "requires": { - "vscode-jsonrpc": "8.2.0", - "vscode-languageserver-types": "3.17.5" - } - }, - "vscode-languageserver-textdocument": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", - "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==" - }, - "vscode-languageserver-types": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", - "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" - }, - "vscode-uri": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", - "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==" - } - } -} diff --git a/server/package.json b/server/package.json deleted file mode 100644 index 01a28a6..0000000 --- a/server/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "nushell-lsp-server", - "description": "Implementation of the nushell language server in node.", - "author": "Nushell Maintainers", - "license": "MIT", - "version": "1.2.0", - "publisher": "TheNuProjectContributors", - "repository": { - "type": "git", - "url": "https://github.com/nushell/vscode-nushell-lang" - }, - "engines": { - "node": "*" - }, - "dependencies": { - "@types/vscode": "1.93.0", - "tmp": "0.2.3", - "vscode-languageserver": "9.0.1", - "vscode-languageserver-textdocument": "1.0.12", - "vscode-uri": "3.0.8" - }, - "devDependencies": { - "@types/tmp": "0.2.6" - }, - "scripts": {} -} diff --git a/server/src/server.ts b/server/src/server.ts deleted file mode 100644 index 0de255e..0000000 --- a/server/src/server.ts +++ /dev/null @@ -1,669 +0,0 @@ -/* -------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - * ------------------------------------------------------------------------------------------ */ -import { - CompletionItem, - CompletionItemKind, - Definition, - Diagnostic, - DiagnosticSeverity, - DidChangeConfigurationNotification, - HandlerResult, - HoverParams, - InitializeParams, - InitializeResult, - ProposedFeatures, - TextDocumentPositionParams, - TextDocumentSyncKind, - TextDocuments, - createConnection, -} from 'vscode-languageserver/node'; - -import { - InlayHint, - InlayHintKind, - InlayHintLabelPart, - InlayHintParams, - Position, -} from 'vscode-languageserver-protocol'; - -import { TextEncoder } from 'node:util'; -import { TextDocument } from 'vscode-languageserver-textdocument'; -import { URI } from 'vscode-uri'; - -interface NuTextDocument extends TextDocument { - nuInlayHints?: InlayHint[]; -} - -// import fs = require('fs'); -import * as fs from 'fs'; -// import tmp = require('tmp'); -import * as tmp from 'tmp'; -// import path = require('path'); -import * as path from 'path'; - -// import util = require('node:util'); -import * as util from 'node:util'; -import * as child_process from 'node:child_process'; - -// eslint-disable-next-line @typescript-eslint/no-var-requires -// const exec = util.promisify(require('node:child_process').exec); -const exec = util.promisify(child_process.exec); - -const tmpFile = tmp.fileSync({ prefix: 'nushell', keep: false }); - -// Create a connection for the server, using Node's IPC as a transport. -// Also include all preview / proposed LSP features. -const connection = createConnection(ProposedFeatures.all); - -// Create a simple text document manager. -const documents: TextDocuments = new TextDocuments(TextDocument); - -let hasConfigurationCapability = false; -let hasWorkspaceFolderCapability = false; -let hasDiagnosticRelatedInformationCapability = false; - -function includeFlagForPath(file_path: string): string { - const parsed = URI.parse(file_path); - if (parsed.scheme === 'file') { - return '-I ' + '"' + path.dirname(parsed.fsPath); - } - return '-I ' + '"' + file_path; -} - -connection.onExit(() => { - tmpFile.removeCallback(); -}); - -connection.onInitialize((params: InitializeParams) => { - const capabilities = params.capabilities; - - // Does the client support the `workspace/configuration` request? - // If not, we fall back using global settings. - hasConfigurationCapability = !!( - capabilities.workspace && !!capabilities.workspace.configuration - ); - hasWorkspaceFolderCapability = !!( - capabilities.workspace && !!capabilities.workspace.workspaceFolders - ); - hasDiagnosticRelatedInformationCapability = !!( - capabilities.textDocument && - capabilities.textDocument.publishDiagnostics && - capabilities.textDocument.publishDiagnostics.relatedInformation - ); - - const result: InitializeResult = { - capabilities: { - textDocumentSync: TextDocumentSyncKind.Incremental, - // Tell the client that this server supports code completion. - completionProvider: { - resolveProvider: true, - }, - inlayHintProvider: { - resolveProvider: false, - }, - hoverProvider: true, - definitionProvider: true, - }, - }; - if (hasWorkspaceFolderCapability) { - result.capabilities.workspace = { - workspaceFolders: { - supported: true, - }, - }; - } - return result; -}); - -let labelid = 0; -function createLabel(name: string): string { - return `${name}#${labelid++}`; -} -async function durationLogWrapper( - name: string, - fn: (label: string) => Promise, -): Promise { - console.log('Triggered ' + name + ': ...'); - const label = createLabel(name); - console.time(label); - const result = await fn(label); - - // This purposefully has the same prefix length as the "Triggered " log above, - // also does not add a newline at the end. - process.stdout.write('Finished '); - console.timeEnd(label); - return new Promise((resolve) => resolve(result)); -} - -connection.onInitialized(() => { - if (hasConfigurationCapability) { - // Register for all configuration changes. - connection.client.register( - DidChangeConfigurationNotification.type, - undefined, - ); - } - if (hasWorkspaceFolderCapability) { - connection.workspace.onDidChangeWorkspaceFolders((_event) => { - connection.console.log('Workspace folder change event received.'); - }); - } -}); - -// The nushell settings -interface NushellIDESettings { - maxNumberOfProblems: number; - hints: { - showInferredTypes: boolean; - }; - nushellExecutablePath: string; - maxNushellInvocationTime: number; - includeDirs: string[]; -} - -// The global settings, used when the `workspace/configuration` request is not supported by the client. -// Please note that this is not the case when using this server with the client provided in this example -// but could happen with other clients. -const defaultSettings: NushellIDESettings = { - maxNumberOfProblems: 1000, - hints: { showInferredTypes: true }, - nushellExecutablePath: 'nu', - maxNushellInvocationTime: 10000000, - includeDirs: [], -}; -let globalSettings: NushellIDESettings = defaultSettings; - -// Cache the settings of all open documents -const documentSettings: Map> = new Map(); - -connection.onDidChangeConfiguration((change) => { - if (hasConfigurationCapability) { - // Reset all cached document settings - documentSettings.clear(); - } else { - globalSettings = ( - (change.settings.nushellLanguageServer || defaultSettings) - ); - } - - // Revalidate all open text documents - documents.all().forEach(validateTextDocument); -}); - -function getDocumentSettings(resource: string): Thenable { - if (!hasConfigurationCapability) { - return Promise.resolve(globalSettings); - } - let result = documentSettings.get(resource); - if (!result) { - result = connection.workspace.getConfiguration({ - scopeUri: resource, - section: 'nushellLanguageServer', - }); - documentSettings.set(resource, result); - } - return result; -} - -// Only keep settings for open documents -documents.onDidClose((e) => { - documentSettings.delete(e.document.uri); -}); - -function debounce(func: any, wait: number, immediate: boolean) { - let timeout: any; - - return function executedFunction(this: any, ...args: any[]) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - const context = this; - - const later = function () { - timeout = null; - if (!immediate) func.apply(context, args); - }; - - const callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) func.apply(context, args); - }; -} - -documents.onDidChangeContent( - (() => { - const throttledValidateTextDocument = debounce( - validateTextDocument, - 500, - false, - ); - - return (change) => { - throttledValidateTextDocument(change.document); - }; - })(), -); - -async function validateTextDocument( - textDocument: NuTextDocument, -): Promise { - return await durationLogWrapper( - `validateTextDocument ${textDocument.uri}`, - async (label) => { - if (!hasDiagnosticRelatedInformationCapability) { - console.error( - 'Trying to validate a document with no diagnostic capability', - ); - return; - } - - // In this simple example we get the settings for every validate run. - const settings = await getDocumentSettings(textDocument.uri); - - // The validator creates diagnostics for all uppercase words length 2 and more - const text = textDocument.getText(); - const lineBreaks = findLineBreaks(text); - - const stdout = await runCompiler( - text, - '--ide-check', - settings, - textDocument.uri, - { label: label }, - ); - - textDocument.nuInlayHints = []; - const diagnostics: Diagnostic[] = []; - - // FIXME: We use this to deduplicate type hints given by the compiler. - // It'd be nicer if it didn't give duplicate hints in the first place. - const seenTypeHintPositions = new Set(); - - const lines = stdout.split('\n').filter((l) => l.length > 0); - for (const line of lines) { - connection.console.log('line: ' + line); - try { - const obj = JSON.parse(line); - - if (obj.type == 'diagnostic') { - let severity: DiagnosticSeverity = DiagnosticSeverity.Error; - - switch (obj.severity) { - case 'Information': - severity = DiagnosticSeverity.Information; - break; - case 'Hint': - severity = DiagnosticSeverity.Hint; - break; - case 'Warning': - severity = DiagnosticSeverity.Warning; - break; - case 'Error': - severity = DiagnosticSeverity.Error; - break; - } - - const position_start = convertSpan(obj.span.start, lineBreaks); - const position_end = convertSpan(obj.span.end, lineBreaks); - - const diagnostic: Diagnostic = { - severity, - range: { - start: position_start, - end: position_end, - }, - message: obj.message, - source: textDocument.uri, - }; - - // connection.console.log(diagnostic.message); - - diagnostics.push(diagnostic); - } else if (obj.type == 'hint' && settings.hints.showInferredTypes) { - if (!seenTypeHintPositions.has(obj.position)) { - seenTypeHintPositions.add(obj.position); - const position = convertSpan(obj.position.end, lineBreaks); - const hint_string = ': ' + obj.typename; - const hint = InlayHint.create( - position, - [InlayHintLabelPart.create(hint_string)], - InlayHintKind.Type, - ); - - textDocument.nuInlayHints.push(hint); - } - } - } catch (e) { - connection.console.error(`error: ${e}`); - } - } - - // Send the computed diagnostics to VSCode. - connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); - }, - ); -} - -connection.onDidChangeWatchedFiles((_change) => { - // Monitored files have change in VSCode - connection.console.log('We received an file change event'); -}); - -function lowerBoundBinarySearch(arr: number[], num: number): number { - let low = 0; - let mid = 0; - let high = arr.length - 1; - - if (num >= arr[high]) return high; - - while (low < high) { - // Bitshift to avoid floating point division - mid = (low + high) >> 1; - - if (arr[mid] < num) { - low = mid + 1; - } else { - high = mid; - } - } - - return low - 1; -} - -function convertSpan(utf8_offset: number, lineBreaks: Array): Position { - const lineBreakIndex = lowerBoundBinarySearch(lineBreaks, utf8_offset); - - const start_of_line_offset = - lineBreakIndex == -1 ? 0 : lineBreaks[lineBreakIndex] + 1; - const character = Math.max(0, utf8_offset - start_of_line_offset); - - return { line: lineBreakIndex + 1, character }; -} - -function convertPosition(position: Position, text: string): number { - let line = 0; - let character = 0; - const buffer = new TextEncoder().encode(text); - - let i = 0; - while (i < buffer.length) { - if (line == position.line && character == position.character) { - return i; - } - - if (buffer.at(i) == 0x0a) { - line++; - character = 0; - } else { - character++; - } - - i++; - } - - return i; -} - -async function runCompiler( - text: string, // this is the script or the snippet of nushell code - flags: string, - settings: NushellIDESettings, - uri: string, - options: { allowErrors?: boolean; label: string } = { label: 'runCompiler' }, -): Promise { - const allowErrors = - options.allowErrors === undefined ? true : options.allowErrors; - - try { - fs.writeFileSync(tmpFile.name, text); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (e: any) { - connection.console.error( - `[${options.label}] error writing to tmp file: ${e}`, - ); - } - - let stdout = ''; - try { - const script_path_flag = - includeFlagForPath(uri) + - '\x1e' + // \x1e is the record separator character (a character that is unlikely to appear in a path) - settings.includeDirs.join('\x1e') + - '"'; - - const max_errors = settings.maxNumberOfProblems; - - if (flags.includes('ide-check')) { - flags = flags + ' ' + max_errors; - } - - connection.console.log( - `[${options.label}] running: ${settings.nushellExecutablePath} ${flags} ${script_path_flag} ${tmpFile.name}`, - ); - - const output = await exec( - `${settings.nushellExecutablePath} ${flags} ${script_path_flag} ${tmpFile.name}`, - { - timeout: settings.maxNushellInvocationTime, - }, - ); - stdout = output.stdout; - console.log(`[${options.label}] stdout: ${stdout}`); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (e: any) { - stdout = e.stdout; - connection.console.log(`[${options.label}] compile failed: ` + e); - if (!allowErrors) { - throw e; - } - } - - return stdout; -} - -connection.onHover(async (request: HoverParams) => { - return await durationLogWrapper(`onHover`, async () => { - const document = documents.get(request.textDocument.uri); - const settings = await getDocumentSettings(request.textDocument.uri); - - const text = document?.getText(); - - if (!(typeof text == 'string')) return null; - - // connection.console.log("request: "); - // connection.console.log(request.textDocument.uri); - // connection.console.log("index: " + convertPosition(request.position, text)); - const stdout = await runCompiler( - text, - '--ide-hover ' + convertPosition(request.position, text), - settings, - request.textDocument.uri, - ); - - const lines = stdout.split('\n').filter((l) => l.length > 0); - for (const line of lines) { - const obj = JSON.parse(line); - // connection.console.log("hovering"); - // connection.console.log(obj); - - // FIXME: Figure out how to import `vscode` package in server.ts without - // getting runtime import errors to remove this deprecation warning. - const contents = { - value: obj.hover, - kind: 'markdown', - }; - - if (obj.hover != '') { - if (obj.span) { - const lineBreaks = findLineBreaks( - obj.file - ? (await fs.promises.readFile(obj.file)).toString() - : (document?.getText() ?? ''), - ); - - return { - contents, - range: { - start: convertSpan(obj.span.start, lineBreaks), - end: convertSpan(obj.span.end, lineBreaks), - }, - }; - } else { - return { contents }; - } - } - } - }); -}); - -// This handler provides the initial list of the completion items. -connection.onCompletion( - async (request: TextDocumentPositionParams): Promise => { - return await durationLogWrapper(`onCompletion`, async () => { - // The pass parameter contains the position of the text document in - // which code complete got requested. For the example we ignore this - // info and always provide the same completion items. - - const document = documents.get(request.textDocument.uri); - const settings = await getDocumentSettings(request.textDocument.uri); - - const text = document?.getText(); - - if (typeof text == 'string') { - // connection.console.log("completion request: "); - // connection.console.log(request.textDocument.uri); - const index = convertPosition(request.position, text); - // connection.console.log("index: " + index); - const stdout = await runCompiler( - text, - '--ide-complete ' + index, - settings, - request.textDocument.uri, - ); - // connection.console.log("got: " + stdout); - - const lines = stdout.split('\n').filter((l) => l.length > 0); - for (const line of lines) { - const obj = JSON.parse(line); - // connection.console.log("completions"); - // connection.console.log(obj); - - const output = []; - let index = 1; - for (const completion of obj.completions) { - output.push({ - label: completion, - kind: completion.includes('(') - ? CompletionItemKind.Function - : CompletionItemKind.Field, - data: index, - }); - index++; - } - return output; - } - } - - return []; - }); - }, -); - -connection.onDefinition(async (request) => { - return await durationLogWrapper(`onDefinition`, async (label) => { - const document = documents.get(request.textDocument.uri); - if (!document) return; - const settings = await getDocumentSettings(request.textDocument.uri); - - const text = document.getText(); - - // connection.console.log(`[${label}] request: ${request.textDocument.uri}`); - // connection.console.log("index: " + convertPosition(request.position, text)); - const stdout = await runCompiler( - text, - '--ide-goto-def ' + convertPosition(request.position, text), - settings, - request.textDocument.uri, - { label: label }, - ); - return goToDefinition(document, stdout); - }); -}); - -// This handler resolves additional information for the item selected in -// the completion list. -connection.onCompletionResolve((item: CompletionItem): CompletionItem => { - return item; -}); - -async function goToDefinition( - document: TextDocument, - nushellOutput: string, -): Promise | undefined> { - const lines = nushellOutput.split('\n').filter((l) => l.length > 0); - for (const line of lines) { - const obj = JSON.parse(line); - // connection.console.log("going to type definition"); - // connection.console.log(obj); - if (obj.file === '' || obj.file === '__prelude__') return; - - let documentText: string; - if (obj.file) { - if (fs.existsSync(obj.file)) { - documentText = await fs.promises - .readFile(obj.file) - .then((b) => b.toString()); - } else { - connection.console.log(`File ${obj.file} does not exist`); - return; - } - } else { - documentText = document.getText(); - } - - const lineBreaks: number[] = findLineBreaks(documentText); - - let uri = ''; - if (obj.file == tmpFile.name) { - uri = document.uri; - } else { - uri = obj.file ? URI.file(obj.file).toString() : document.uri; - } - - // connection.console.log(uri); - - return { - uri: uri, - range: { - start: convertSpan(obj.start, lineBreaks), - end: convertSpan(obj.end, lineBreaks), - }, - }; - } -} - -connection.languages.inlayHint.on((params: InlayHintParams) => { - const document = documents.get(params.textDocument.uri) as NuTextDocument; - return document.nuInlayHints; -}); - -function findLineBreaks(utf16_text: string): Array { - const utf8_text = new TextEncoder().encode(utf16_text); - const lineBreaks: Array = []; - - for (let i = 0; i < utf8_text.length; ++i) { - if (utf8_text[i] == 0x0a) { - lineBreaks.push(i); - } - } - - return lineBreaks; -} - -// Make the text document manager listen on the connection -// for open, change and close text document events -documents.listen(connection); - -// Listen on the connection -connection.listen(); diff --git a/server/tsconfig.json b/server/tsconfig.json deleted file mode 100644 index 00d13d2..0000000 --- a/server/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "target": "es2020", - "lib": ["es2020"], - "module": "commonjs", - "moduleResolution": "node", - "sourceMap": true, - "strict": true, - "outDir": "out", - "rootDir": "src" - }, - "include": ["src"], - "exclude": ["node_modules", ".vscode-test"] -}