Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrating pyFlies Language server into VSCode extension #1

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ node_modules
.vscode-test/
*.vsix
code-extensions/
env/
pyfliesls/
__pycache__/
*.log
95 changes: 67 additions & 28 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
]
}
4 changes: 3 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
9 changes: 3 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,6 @@
"configuration": "./languages/pyflies.language-configuration.json"
}
],
"snippets": [
{
"language": "pyflies",
"path": "./snippets/pyflies-snippets.json"
}
],
"keybindings": [
{
"command": "markdowntable.format",
Expand Down Expand Up @@ -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",
Expand Down
111 changes: 92 additions & 19 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
return client ? client.stop() : Promise.resolve();
}
103 changes: 103 additions & 0 deletions src/setup.ts
Original file line number Diff line number Diff line change
@@ -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<string>("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<string> {
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<string> {
// 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<string> => {
return new Promise<string>((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);
});
});

}