diff --git a/.gitignore b/.gitignore index 04b5b0d..a64e335 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ node_modules .vscode-test/ *.vsix code-extensions/ +env/ +pyfliesls/ +__pycache__/ +*.log diff --git a/.vscode/launch.json b/.vscode/launch.json index ebc23ff..19c0169 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -3,33 +3,72 @@ // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 { - "version": "0.2.0", - "configurations": [ + "version": "0.2.0", + "configurations": [ - { - "name": "Run Extension", - "type": "extensionHost", - "request": "launch", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}" - ], - "outFiles": [ - "${workspaceFolder}/out/**/*.js" - ], - "preLaunchTask": "${defaultBuildTask}" - }, - { - "name": "Extension Tests", - "type": "extensionHost", - "request": "launch", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}", - "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" - ], - "outFiles": [ - "${workspaceFolder}/out/test/**/*.js" - ], - "preLaunchTask": "${defaultBuildTask}" - } - ] + { + "name": "Launch Client", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "preLaunchTask": "${defaultBuildTask}", + "env": { + "VSCODE_DEBUG_MODE": "true" + } + }, + { + "name": "Launch Server", + "type": "python", + "request": "launch", + "module": "server", + "args": ["--tcp"], + "justMyCode": false, + "python": "${command:python.interpreterPath}", + "cwd": "${workspaceFolder}", + "env": { + "PYTHONPATH": "${workspaceFolder}" + } + }, + { + "name": "Launch Server - Context Completion", + "type": "python", + "request": "launch", + "module": "server", + "args": ["--tcp", "--context-completion"], + "justMyCode": false, + "python": "${command:python.interpreterPath}", + "cwd": "${workspaceFolder}", + "env": { + "PYTHONPATH": "${workspaceFolder}" + } + }, + { + "name": "Extension Tests", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" + ], + "outFiles": [ + "${workspaceFolder}/out/test/**/*.js" + ], + "preLaunchTask": "${defaultBuildTask}" + } + ], + "compounds": [ + { + "name": "Server + Client", + "configurations": ["Launch Server", "Launch Client"] + }, + { + "name": "Server (Context Completion) + Client", + "configurations": ["Launch Server - Context Completion", "Launch Client"] + } + ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 30bf8c2..605e8d4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,7 @@ "out": true // set this to false to include "out" folder in search results }, // Turn off tsc task auto detection since we have the necessary tasks as npm scripts - "typescript.tsc.autoDetect": "off" + "typescript.tsc.autoDetect": "off", + + "python.pythonPath": "./pyfliesls/Scripts/python" } \ No newline at end of file diff --git a/package.json b/package.json index 1839020..2f63297 100644 --- a/package.json +++ b/package.json @@ -52,12 +52,6 @@ "configuration": "./languages/pyflies.language-configuration.json" } ], - "snippets": [ - { - "language": "pyflies", - "path": "./snippets/pyflies-snippets.json" - } - ], "keybindings": [ { "command": "markdowntable.format", @@ -128,6 +122,9 @@ "pretest": "yarn run compile && yarn run lint", "test": "node ./out/test/runTest.js" }, + "dependencies": { + "vscode-languageclient": "^7.0.0" + }, "devDependencies": { "@types/vscode": "^1.50.0", "@types/glob": "^7.1.3", diff --git a/src/extension.ts b/src/extension.ts index 69acc98..ecc10be 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,29 +1,102 @@ // The module 'vscode' contains the VS Code extensibility API // Import the module and reference it with the alias vscode in your code below import * as vscode from 'vscode'; +import * as net from "net"; +import { installLSWithProgress } from './setup'; + +import { + LanguageClient, + LanguageClientOptions, + ServerOptions, +} from 'vscode-languageclient/node'; + +let client: LanguageClient; + +function getClientOptions(): LanguageClientOptions { + return { + // Register the server for plain text documents + documentSelector: ["*"], + outputChannelName: "pyFlies", + }; +} + +function isStartedInDebugMode(): boolean { + return process.env.VSCODE_DEBUG_MODE === "true"; +} + +function startLangServerTCP(addr: number): LanguageClient { + const serverOptions: ServerOptions = () => { + return new Promise((resolve /*, reject */) => { + const clientSocket = new net.Socket(); + clientSocket.connect(addr, "127.0.0.1", () => { + resolve({ + reader: clientSocket, + writer: clientSocket, + }); + }); + }); + }; + + // 'pyFlies LS (port ${addr})' + return new LanguageClient( + `tcp lang server (port ${addr})`, + serverOptions, + getClientOptions() + ); +} + +function startLangServer( + command: string, + args: string[], + cwd: string +): LanguageClient { + const serverOptions: ServerOptions = { + args, + command, + options: { cwd }, + }; + + return new LanguageClient(command, serverOptions, getClientOptions()); +} + // this method is called when your extension is activated // your extension is activated the very first time the command is executed -export function activate(context: vscode.ExtensionContext) { - - var mdTableExtension = vscode.extensions.getExtension('TakumiI.markdowntable'); - - if (mdTableExtension !== undefined) { - if (mdTableExtension.isActive === false) { - mdTableExtension.activate().then( - function () { - vscode.window.showInformationMessage("Markdown Table extension activated"); - }, - function () { - vscode.window.showErrorMessage("Markdown Table activation failed"); - } - ); - } - } else { - vscode.window.showErrorMessage("Markdown Table not found!"); - } +export async function activate(context: vscode.ExtensionContext) { + if (isStartedInDebugMode()) { + // Development - Run the server manually + client = startLangServerTCP(parseInt(process.env.SERVER_PORT || "2087")); + } else { + // Production - Client is going to run the server (for use within `.vsix` package) + try { + const python = await installLSWithProgress(context); + client = startLangServer(python, ["-m", "pyflies_ls"], context.extensionPath); + } catch (err:any) { + vscode.window.showErrorMessage(err.toString()); + } + } + + context.subscriptions.push(client.start()); + + var mdTableExtension = vscode.extensions.getExtension('TakumiI.markdowntable'); + + if (mdTableExtension !== undefined) { + if (mdTableExtension.isActive === false) { + mdTableExtension.activate().then( + function () { + vscode.window.showInformationMessage("Markdown Table extension activated"); + }, + function () { + vscode.window.showErrorMessage("Markdown Table activation failed"); + } + ); + } + } else { + vscode.window.showErrorMessage("Markdown Table not found!"); + } } // this method is called when your extension is deactivated -export function deactivate() { +export function deactivate(): Thenable { + return client ? client.stop() : Promise.resolve(); } diff --git a/src/setup.ts b/src/setup.ts new file mode 100644 index 0000000..d2943e3 --- /dev/null +++ b/src/setup.ts @@ -0,0 +1,103 @@ +import { execSync } from "child_process"; +import { existsSync, readdirSync } from "fs"; +import { join } from "path"; +import { ExtensionContext, ProgressLocation, window, workspace} from "vscode"; + +export const IS_WIN = process.platform === "win32"; + +function createVirtualEnvironment(python: string, name: string, cwd: string): string { + const path = join(cwd, name); + if (!existsSync(path)) { + const createVenvCmd = `${python} -m venv ${name}`; + execSync(createVenvCmd, { cwd }); + } + return path; +} + +function getPython(): string { + return workspace.getConfiguration("python").get("pythonPath", getPythonCrossPlatform()); +} + +function getPythonCrossPlatform(): string { + return IS_WIN ? "python" : "python3"; +} + +function getPythonFromVenvPath(venvPath: string): string { + return IS_WIN ? join(venvPath, "Scripts", "python") : join(venvPath, "bin", "python"); +} + +function getPythonVersion(python: string): number[] | undefined { + const getPythonVersionCmd = `${python} --version`; + const version = execSync(getPythonVersionCmd).toString(); + return version.match(RegExp(/\d/g))?.map((v) => Number.parseInt(v)); +} + +function getVenvPackageVersion(python: string, name: string): boolean { + const listPipPackagesCmd = `${python} -m pip show ${name}`; + + try { + const packageInfo = execSync(listPipPackagesCmd).toString(); + if (packageInfo === undefined){ + return false; + } + return true; + } catch (err) { + return false; + } +} + +function installServer(python: string){ + execSync(`${python} -m pip install pyflies-ls`); +} + +function* installLS(context: ExtensionContext): IterableIterator { + yield "Installing textX language server"; + + // Get python interpreter + const python = getPython(); + // Check python version (3.7+ is required) + const [major, minor] = getPythonVersion(python) || [3, 7]; + if (major !== 3 || minor < 7) { + throw new Error("Python 3.7+ is required!"); + } + + // Create virtual environment + const venv = createVirtualEnvironment(python, "pyfliesls", context.extensionPath); + yield `Virtual Environment created in: ${venv}`; + + // Install source from wheels + const venvPython = getPythonFromVenvPath(venv); + installServer(venvPython); + yield `Successfully installed pyflies-LS.`; +} + +export async function installLSWithProgress(context: ExtensionContext): Promise { + // Check if LS is already installed + const venvPython = getPythonFromVenvPath(join(context.extensionPath, "pyfliesls")); + + if (getVenvPackageVersion(venvPython, "pyflies-ls")) { + return Promise.resolve(venvPython); + } + + // Install with progress bar + return await window.withProgress({ + location: ProgressLocation.Window, + }, (progress): Promise => { + return new Promise((resolve, reject) => { + + // Catch generator errors + try { + // Go through installation steps + for (const step of installLS(context)) { + progress.report({ message: step }); + } + + } catch (err) { + reject(err); + } + + resolve(venvPython); + }); + }); + +}