diff --git a/server/core/odoo.py b/server/core/odoo.py index 997d0baf..23469103 100644 --- a/server/core/odoo.py +++ b/server/core/odoo.py @@ -193,7 +193,6 @@ def reload_database(ls): Odoo.get().reset(ls) FileMgr.files = {} ls.show_message_log("Building new database", MessageType.Log) - ls.show_message("Reloading Odoo database", MessageType.Info) ls.launch_thread(target=Odoo.initialize, args=(ls,)) diff --git a/vscode/build_package.sh b/vscode/build_package.sh index f5447149..40c1fd3d 100755 --- a/vscode/build_package.sh +++ b/vscode/build_package.sh @@ -1,3 +1,5 @@ +#!/bin/bash + PACKAGE_VERSION=$(cat package.json \ | grep version \ | head -1 \ diff --git a/vscode/client/utils/cleanup.mjs b/vscode/client/common/cleanup.mjs similarity index 100% rename from vscode/client/utils/cleanup.mjs rename to vscode/client/common/cleanup.mjs diff --git a/vscode/client/common/constants.ts b/vscode/client/common/constants.ts new file mode 100644 index 00000000..6a018740 --- /dev/null +++ b/vscode/client/common/constants.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; + +const folderName = path.basename(__dirname); +export const EXTENSION_ROOT_DIR = + folderName === 'common' ? path.dirname(path.dirname(__dirname)) : path.dirname(__dirname); +export const BUNDLED_PYTHON_SCRIPTS_DIR = path.join(EXTENSION_ROOT_DIR, 'server'); +export const SERVER_SCRIPT_PATH = path.join(BUNDLED_PYTHON_SCRIPTS_DIR, '__main__.py'); +export const DEBUG_SERVER_SCRIPT_PATH = path.join(BUNDLED_PYTHON_SCRIPTS_DIR, `_debug_server.py`); \ No newline at end of file diff --git a/vscode/client/utils/events.ts b/vscode/client/common/events.ts similarity index 76% rename from vscode/client/utils/events.ts rename to vscode/client/common/events.ts index b3fa16a9..b6f85fe2 100644 --- a/vscode/client/utils/events.ts +++ b/vscode/client/common/events.ts @@ -2,3 +2,4 @@ import {EventEmitter} from "vscode"; export const selectedConfigurationChange = new EventEmitter(); export const ConfigurationsChange = new EventEmitter(); +export const clientStopped = new EventEmitter(); diff --git a/vscode/client/common/python.ts b/vscode/client/common/python.ts new file mode 100644 index 00000000..b23e9e16 --- /dev/null +++ b/vscode/client/common/python.ts @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/* eslint-disable @typescript-eslint/naming-convention */ +import { commands, Disposable, Event, EventEmitter, Uri } from 'vscode'; +import { PythonExtension, ResolvedEnvironment, VersionInfo } from '@vscode/python-extension'; + +export interface IInterpreterDetails { + path?: string[]; + resource?: Uri; +} + +export const onDidChangePythonInterpreterEvent = new EventEmitter(); +export const onDidChangePythonInterpreter: Event = onDidChangePythonInterpreterEvent.event; + +let _api: PythonExtension | undefined; +async function getPythonExtensionAPI(): Promise { + if (_api) { + return _api; + } + _api = await PythonExtension.api(); + return _api; +} + +export async function initializePython(disposables: Disposable[]): Promise { + try { + const api = await getPythonExtensionAPI(); + + if (api) { + disposables.push( + api.environments.onDidChangeActiveEnvironmentPath((e) => { + onDidChangePythonInterpreterEvent.fire({ path: [e.path], resource: e.resource?.uri }); + }), + ); + + console.log('Waiting for interpreter from python extension.'); + onDidChangePythonInterpreterEvent.fire(await getInterpreterDetails()); + } + } catch (error) { + console.error('Error initializing python: ', error); + } +} + +export async function resolveInterpreter(interpreter: string[]): Promise { + const api = await getPythonExtensionAPI(); + return api?.environments.resolveEnvironment(interpreter[0]); +} + +export async function getInterpreterDetails(resource?: Uri): Promise { + const api = await getPythonExtensionAPI(); + const environment = await api?.environments.resolveEnvironment( + api?.environments.getActiveEnvironmentPath(resource), + ); + if (environment?.executable.uri && checkVersion(environment)) { + return { path: [environment?.executable.uri.fsPath], resource }; + } + return { path: undefined, resource }; +} + +export async function getDebuggerPath(): Promise { + const api = await getPythonExtensionAPI(); + return api?.debug.getDebuggerPackagePath(); +} + +export async function runPythonExtensionCommand(command: string, ...rest: any[]) { + await getPythonExtensionAPI(); + return await commands.executeCommand(command, ...rest); +} + +export function checkVersion(resolved: ResolvedEnvironment | undefined): boolean { + const version = resolved?.version; + if (version?.major === 3 && version?.minor >= 8) { + return true; + } + console.error(`Python version ${version?.major}.${version?.minor} is not supported.`); + console.error(`Selected python path: ${resolved?.executable.uri?.fsPath}`); + console.error('Supported versions are 3.8 and above.'); + return false; +} + +export async function getPythonVersion(): Promise { + let interpreterDetails = await getInterpreterDetails(); + let resolved = await resolveInterpreter(interpreterDetails.path); + let version = resolved.version; + + return version; +} diff --git a/vscode/client/utils/utils.ts b/vscode/client/common/utils.ts similarity index 79% rename from vscode/client/utils/utils.ts rename to vscode/client/common/utils.ts index f6d62ad4..a4aafa36 100644 --- a/vscode/client/utils/utils.ts +++ b/vscode/client/common/utils.ts @@ -1,4 +1,3 @@ -import { execSync } from "child_process"; import { ExtensionContext, Uri, Webview } from "vscode"; /** @@ -25,17 +24,9 @@ export function getNonce() { return text; } -export function getPythonVersion(context: ExtensionContext, pythonPath: String = "python3") { - try { - return execSync(`${pythonPath} --version`, {encoding: 'utf8'}) - } catch { - return null; - } -} - // Config related utils -export function getCurrentConfig(context: ExtensionContext) { +export async function getCurrentConfig(context: ExtensionContext) { const configs: any = context.globalState.get("Odoo.configurations"); const activeConfig: number = Number(context.workspaceState.get('Odoo.selectedConfiguration')); return (configs && activeConfig > -1 ? configs[activeConfig] : null); diff --git a/vscode/client/utils/validation.ts b/vscode/client/common/validation.ts similarity index 91% rename from vscode/client/utils/validation.ts rename to vscode/client/common/validation.ts index e672d324..832124ff 100644 --- a/vscode/client/utils/validation.ts +++ b/vscode/client/common/validation.ts @@ -12,6 +12,5 @@ export function getConfigurationStructure(id: number = 0) { "name": `New Configuration ${id}`, "odooPath": "", "addons": [], - "pythonPath": "python3", } } diff --git a/vscode/client/extension.ts b/vscode/client/extension.ts index 46ed1270..a9701518 100644 --- a/vscode/client/extension.ts +++ b/vscode/client/extension.ts @@ -10,7 +10,6 @@ import { ExtensionMode, QuickPickItem, StatusBarAlignment, - StatusBarItem, ThemeIcon, workspace, window, @@ -31,16 +30,20 @@ import { CrashReportWebView } from './views/crash_report/crashReport'; import { ChangelogWebview } from "./views/changelog/changelogWebview"; import { selectedConfigurationChange, - ConfigurationsChange -} from './utils/events' + ConfigurationsChange, + clientStopped +} from './common/events' +import { + IInterpreterDetails, + getInterpreterDetails, + initializePython, + onDidChangePythonInterpreter, + onDidChangePythonInterpreterEvent +} from "./common/python"; +import { getCurrentConfig } from "./common/utils"; +import { getConfigurationStructure, stateInit } from "./common/validation"; import { execSync } from "child_process"; -import { getCurrentConfig } from "./utils/utils"; -import { getConfigurationStructure, stateInit } from "./utils/validation"; -import * as os from 'os'; -let client: LanguageClient; -let odooStatusBar: StatusBarItem; -let isLoading: boolean; -let debugFile = `pygls-${new Date().toISOString().replaceAll(":","_")}.log` + function getClientOptions(): LanguageClientOptions { return { @@ -50,13 +53,11 @@ function getClientOptions(): LanguageClientOptions { { scheme: "untitled", language: "python" }, ], synchronize: { - // Notify the server about file changes to '.clientrc files contain in the workspace - fileEvents: workspace.createFileSystemWatcher("**/.clientrc"), }, }; } -function validateState(context: ExtensionContext, outputChannel: OutputChannel) { +function validateState(context: ExtensionContext) { try { let globalState = context.globalState let stateVersion = globalState.get('Odoo.stateVersion', false) @@ -98,12 +99,12 @@ function validateState(context: ExtensionContext, outputChannel: OutputChannel) } catch (error) { - outputChannel.appendLine(error) - displayCrashMessage(context, error, outputChannel, 'func.validateState') + global.LSCLIENT.error(error); + displayCrashMessage(context, error, 'func.validateState') } } -function setMissingStateVariables(context: ExtensionContext, outputChannel: OutputChannel) { +function setMissingStateVariables(context: ExtensionContext) { const globalStateKeys = context.globalState.keys(); const workspaceStateKeys = context.workspaceState.keys(); let globalVariables = new Map([ @@ -116,33 +117,19 @@ function setMissingStateVariables(context: ExtensionContext, outputChannel: Outp for (let key of globalVariables.keys()) { if (!globalStateKeys.includes(key)) { - outputChannel.appendLine(`${key} was missing in global state. Setting up the variable.`); + global.LSCLIENT.info(`${key} was missing in global state. Setting up the variable.`); context.globalState.update(key, globalVariables.get(key)); } } for (let key of workspaceVariables.keys()) { if (!workspaceStateKeys.includes(key)) { - outputChannel.appendLine(`${key} was missing in workspace state. Setting up the variable.`); + global.LSCLIENT.info(`${key} was missing in workspace state. Setting up the variable.`); context.workspaceState.update(key, workspaceVariables.get(key)); } } } -function checkPythonVersion(pythonPath: string) { - const versionString = execSync(`${pythonPath} --version`).toString().replace("Python ", "") - const pythonVersion = semver.parse(versionString) - if (semver.lt(pythonVersion, "3.8.0")) return false - return true -} - -function displayInvalidPythonError(context: ExtensionContext) { - const selection = window.showErrorMessage( - "Unable to start the Odoo Language Server. Python 3.8+ is required.", - "Dismiss" - ); -} - function isExtensionUpdated(context: ExtensionContext) { const currentSemVer = semver.parse(context.extension.packageJSON.version); const lastRecordedSemVer = semver.parse(context.globalState.get("Odoo.lastRecordedVersion", "")); @@ -151,18 +138,17 @@ function isExtensionUpdated(context: ExtensionContext) { return false; } -function displayUpdatedNotification(context: ExtensionContext) { - window.showInformationMessage( +async function displayUpdatedNotification(context: ExtensionContext) { + const selection = await window.showInformationMessage( "The Odoo extension has been updated.", "Show changelog", "Dismiss" - ).then(selection => { - switch (selection) { - case "Show changelog": - ChangelogWebview.render(context); - break; - } - }) + ) + switch (selection) { + case "Show changelog": + ChangelogWebview.render(context); + break; + } } function updateLastRecordedVersion(context: ExtensionContext) { @@ -211,10 +197,10 @@ function startLangServer( return new LanguageClient('odooServer', 'Odoo Server', serverOptions, clientOptions); } -function setStatusConfig(context: ExtensionContext, statusItem: StatusBarItem) { - const config = getCurrentConfig(context); +async function setStatusConfig(context: ExtensionContext) { + const config = await getCurrentConfig(context); let text = (config ? `Odoo (${config["name"]})` : `Odoo (Disabled)`); - statusItem.text = (isLoading) ? "$(loading~spin) " + text : text; + global.STATUS_BAR.text = (global.IS_LOADING) ? "$(loading~spin) " + text : text; } function getLastConfigId(context: ExtensionContext): number | undefined { @@ -236,17 +222,17 @@ async function addNewConfiguration(context: ExtensionContext) { [configId]: getConfigurationStructure(configId), } ); - ConfigurationsChange.fire(null); IncrementLastConfigId(context); ConfigurationWebView.render(context, configId); } -function changeSelectedConfig(context: ExtensionContext, configId: Number) { - context.workspaceState.update("Odoo.selectedConfiguration", configId); - selectedConfigurationChange.fire(null); +async function changeSelectedConfig(context: ExtensionContext, configId: Number) { + const oldConfig = await getCurrentConfig(context) + await context.workspaceState.update("Odoo.selectedConfiguration", configId); + selectedConfigurationChange.fire(oldConfig); } -async function displayCrashMessage(context: ExtensionContext, crashInfo: string, outputChannel: OutputChannel, command: string = null) { +async function displayCrashMessage(context: ExtensionContext, crashInfo: string, command: string = null, outputChannel = global.LSCLIENT.outputChannel) { // Capture the content of the file active when the crash happened let activeFile: TextDocument; if (window.activeTextEditor) { @@ -263,7 +249,7 @@ async function displayCrashMessage(context: ExtensionContext, crashInfo: string, switch (selection) { case ("Send crash report"): - CrashReportWebView.render(context, activeFile, crashInfo, command, debugFile); + CrashReportWebView.render(context, activeFile, crashInfo, command, global.DEBUG_FILE); break case ("Open logs"): outputChannel.show(); @@ -271,62 +257,68 @@ async function displayCrashMessage(context: ExtensionContext, crashInfo: string, } } -function activateVenv(pythonPath: String) { - let activatePathArray = pythonPath.split(path.sep).slice(0, pythonPath.split(path.sep).length - 1) - let activatePath = `${activatePathArray.join(path.sep)}${path.sep}activate` - if (fs.existsSync(activatePath)) { - switch (os.type()) { - case 'Linux': - execSync(`. ${activatePath}`) - break; - case 'Windows_NT': - execSync(`${activatePath}`) - break; +async function initLanguageServerClient(context: ExtensionContext, outputChannel: OutputChannel, autoStart = false) { + let client : LanguageClient; + try { + const pythonPath = await getPythonPath(context); + + if (context.extensionMode === ExtensionMode.Development) { + // Development - Run the server manually + await commands.executeCommand('setContext', 'odoo.showCrashNotificationCommand', true); + client = startLangServerTCP(2087, outputChannel); + global.DEBUG_FILE = 'pygls.log'; + } else { + // Production - Client is going to run the server (for use within `.vsix` package) + const cwd = path.join(__dirname, "..", ".."); + client = startLangServer(pythonPath, ["-m", "server", "--log", global.DEBUG_FILE, "--id", "clean-odoo-lsp"], cwd, outputChannel); } - } -} -function getPythonPath(context: ExtensionContext) { - const config = getCurrentConfig(context); - const pythonPath = config && config["pythonPath"] ? config["pythonPath"] : "python3"; - activateVenv(pythonPath) - return pythonPath -} - -function startLanguageServerClient(context: ExtensionContext, pythonPath:string, outputChannel: OutputChannel) { - let client: LanguageClient; - if (context.extensionMode === ExtensionMode.Development) { - // Development - Run the server manually - client = startLangServerTCP(2087, outputChannel); - debugFile='pygls.log' - } else { - // Production - Client is going to run the server (for use within `.vsix` package) - const cwd = path.join(__dirname, "..", ".."); - - if (!pythonPath) { - outputChannel.appendLine("[INFO] pythonPath is not set, defaulting to python3."); + context.subscriptions.push( + client.onNotification("Odoo/loadingStatusUpdate", async (state: String) => { + switch (state) { + case "start": + global.IS_LOADING = true; + break; + case "stop": + global.IS_LOADING = false; + break; + } + await setStatusConfig(context); + }), + client.onRequest("Odoo/getConfiguration", async (params) => { + return await getCurrentConfig(context); + }), + client.onNotification("Odoo/displayCrashNotification", async (params) => { + await displayCrashMessage(context, params["crashInfo"]); + }) + ); + if (autoStart) { + await client.start(); + await client.sendNotification("Odoo/clientReady"); } - client = startLangServer(pythonPath, ["-m", "server", "--log", debugFile, "--id", "clean-odoo-lsp"], cwd, outputChannel); + return client; + } catch (error) { + outputChannel.appendLine("Couldn't Start Language server."); + outputChannel.appendLine(error); + await displayCrashMessage(context, error, 'initLanguageServer' , outputChannel); } - - return client; } -function deleteOldFiles(context: ExtensionContext, outputChannel: OutputChannel) { +function deleteOldFiles(context: ExtensionContext) { const files = fs.readdirSync(context.extensionUri.fsPath).filter(fn => fn.startsWith('pygls-') && fn.endsWith('.log')); for (const file of files) { - let dateLimit = new Date() + let dateLimit = new Date(); dateLimit.setDate(dateLimit.getDate() - 2); - let date = new Date(file.slice(6, -4).replaceAll("_",":")) + let date = new Date(file.slice(6, -4).replaceAll("_",":")); if (date < dateLimit) { - fs.unlinkSync(Uri.joinPath(context.extensionUri, file).fsPath) + fs.unlinkSync(Uri.joinPath(context.extensionUri, file).fsPath); } } } -async function checkAddons(context: ExtensionContext, odooOutputChannel: OutputChannel) { +async function checkAddons(context: ExtensionContext) { let files = await workspace.findFiles('**/__manifest__.py') - let currentConfig = getCurrentConfig(context) + let currentConfig = await getCurrentConfig(context); if (currentConfig) { let missingFiles = files.filter(file => { return !( @@ -339,94 +331,209 @@ async function checkAddons(context: ExtensionContext, odooOutputChannel: OutputC return filePath.slice(0, filePath.length - 2).join(path.sep) }))] if (missingPaths.length > 0) { - odooOutputChannel.appendLine("Missing addon paths : " + JSON.stringify(missingPaths)) - const selection = await window.showWarningMessage( + global.LSCLIENT.warn("Missing addon paths : " + JSON.stringify(missingPaths)) + window.showWarningMessage( `We detected addon paths that weren't added in the current configuration. Would you like to add them?`, "Update current configuration", "View Paths", "Ignore" - ); - switch (selection) { - case ("Update current configuration"): - ConfigurationWebView.render(context, currentConfig.id); - break - case ("View Paths"): - odooOutputChannel.show(); - break - } + ).then(selection => { + switch (selection) { + case ("Update current configuration"): + ConfigurationWebView.render(context, currentConfig.id); + break + case ("View Paths"): + global.LSCLIENT.outputChannel.show(); + break + } + }); } } } async function checkOdooPath(context: ExtensionContext) { - let currentConfig = getCurrentConfig(context) + let currentConfig = await getCurrentConfig(context); let odooFound = currentConfig ? workspace.getWorkspaceFolder(Uri.parse(currentConfig.odooPath)) : true if (!odooFound) { let invalidPath = false for (const f of workspace.workspaceFolders) { if (fs.existsSync(Uri.joinPath(f.uri, 'odoo-bin').fsPath) || fs.existsSync(Uri.joinPath(Uri.joinPath(f.uri, 'odoo'), 'odoo-bin').fsPath)) { - invalidPath = true - break + invalidPath = true; + break; } } if (invalidPath) { - const selection = await window.showWarningMessage( + window.showWarningMessage( `The Odoo configuration selected does not match the odoo path in the workspace. Would you like to change it?`, "Update current configuration", "Ignore" - ); + ).then(selection => { switch (selection) { case ("Update current configuration"): ConfigurationWebView.render(context, currentConfig.id); break - } + } + }) } } } -function initializeSubscriptions(context: ExtensionContext, client: LanguageClient, odooOutputChannel: OutputChannel): void { +async function initStatusBar(context: ExtensionContext): Promise { + global.STATUS_BAR = window.createStatusBarItem(StatusBarAlignment.Left, 100); + global.STATUS_BAR.command = "odoo.clickStatusBar" + context.subscriptions.push(global.STATUS_BAR); + await setStatusConfig(context); + global.STATUS_BAR.show(); +} + +async function initializeSubscriptions(context: ExtensionContext): Promise { let terminal = window.terminals.find(t => t.name === 'close-odoo-client') if (!terminal){ window.createTerminal({ name: `close-odoo-client`, hideFromUser:true}) } - context.subscriptions.push(window.onDidCloseTerminal((terminal) => { - if (terminal.name === 'close-odoo-client') closeClient(client) + context.subscriptions.push(window.onDidCloseTerminal(async (terminal) => { + if (terminal.name === 'close-odoo-client') await stopClient(); })) - function checkRestartPythonServer(){ - if (getCurrentConfig(context)) { - let oldPythonPath = pythonPath - pythonPath = getPythonPath(context); - if (oldPythonPath != pythonPath) { - odooOutputChannel.appendLine('[INFO] Python path changed, restarting language server: ' + oldPythonPath + " " + pythonPath); - closeClient(client) - client = startLanguageServerClient(context, pythonPath, odooOutputChannel); - for (const disposable of context.subscriptions) { - try { - disposable.dispose(); - } catch (e) { - console.error(e); + // Listen to changes to Configurations + context.subscriptions.push( + ConfigurationsChange.event(async (changes: Array | null) => { + try { + let client = global.LSCLIENT; + await setStatusConfig(context); + const RELOAD_ON_CHANGE = ["odooPath","addons","pythonPath"]; + if (changes && (changes.some(r=> RELOAD_ON_CHANGE.includes(r)))) { + await checkOdooPath(context); + await checkAddons(context); + if (client.diagnostics) client.diagnostics.clear(); + + if (changes.includes('pythonPath')){ + await checkStandalonePythonVersion(context); + onDidChangePythonInterpreterEvent.fire(changes["pythonPath"]); + return } + await client.sendNotification("Odoo/configurationChanged"); + } + } + catch (error) { + global.LSCLIENT.error(error) + await displayCrashMessage(context, 'event.ConfigurationsChange') + } + }) + ); + + // Listen to changes to the selected Configuration + context.subscriptions.push( + selectedConfigurationChange.event(async (oldConfig) => { + try { + if (!global.CAN_QUEUE_CONFIG_CHANGE) return; + + if (global.CLIENT_IS_STOPPING) { + global.CAN_QUEUE_CONFIG_CHANGE = false; + await waitForClientStop(); + global.CAN_QUEUE_CONFIG_CHANGE = true; } - initializeSubscriptions(context, client, odooOutputChannel) - if (checkPythonVersion(pythonPath)) { - client.start(); + + let client = global.LSCLIENT; + const config = await getCurrentConfig(context) + if (config) { + await checkOdooPath(context); + await checkAddons(context); + if (!global.IS_PYTHON_EXTENSION_READY){ + await checkStandalonePythonVersion(context); + if (!oldConfig || config["pythonPath"] != oldConfig["pythonPath"]){ + onDidChangePythonInterpreterEvent.fire(config["pythonPath"]); + await setStatusConfig(context); + return + } + } + if (!client) { + global.LSCLIENT = await initLanguageServerClient(context, global.OUTPUT_CHANNEL); + client = global.LSCLIENT; + } + if (client.needsStart()) { + await client.start(); + await client.sendNotification( + "Odoo/clientReady", + ); + } else { + if (client.diagnostics) client.diagnostics.clear(); + await client.sendNotification("Odoo/configurationChanged"); + } } else { - displayInvalidPythonError(context); + if (client?.isRunning()) await stopClient(); + global.IS_LOADING = false; } + await setStatusConfig(context); } - } - } + catch (error) { + global.LSCLIENT.error(error); + await displayCrashMessage(context, error, 'event.selectedConfigurationChange'); + } + }) + ); + + // Listen to changes to Python Interpreter + context.subscriptions.push( + onDidChangePythonInterpreter(async (e: IInterpreterDetails) => { + let startClient = false; + global.CAN_QUEUE_CONFIG_CHANGE = false; + if (global.LSCLIENT) { + if (global.CLIENT_IS_STOPPING) { + await waitForClientStop(); + } + if (global.LSCLIENT?.isRunning()) { + await stopClient(); + } + await global.LSCLIENT.dispose(); + } + if (await getCurrentConfig(context)) { + startClient = true; + } + global.LSCLIENT = await initLanguageServerClient(context, global.OUTPUT_CHANNEL, startClient); + global.CAN_QUEUE_CONFIG_CHANGE = true; + }) + ); - let pythonPath = getPythonPath(context); - odooStatusBar = window.createStatusBarItem(StatusBarAlignment.Left, 100); - setStatusConfig(context, odooStatusBar); - odooStatusBar.show(); - odooStatusBar.command = "odoo.clickStatusBar" - context.subscriptions.push(odooStatusBar); + // COMMANDS + context.subscriptions.push( + commands.registerCommand("odoo.openWelcomeView", async () => { + try { + WelcomeWebView.render(context); + } + catch (error) { + global.LSCLIENT.error(error) + await displayCrashMessage(context, error, 'odoo.openWelcomeView') + } + }) + ); + + context.subscriptions.push( + commands.registerCommand("odoo.clearState", async () => { + try { + for (let key of context.globalState.keys()) { + global.LSCLIENT.info(`Wiping ${key} from global storage.`); + await context.globalState.update(key, undefined); + } + + for (let key of context.workspaceState.keys()) { + global.LSCLIENT.info(`Wiping ${key} from workspace storage.`); + await context.workspaceState.update(key, undefined); + } + await commands.executeCommand("workbench.action.reloadWindow"); + } + catch (error) { + global.LSCLIENT.error(error); + await displayCrashMessage(context, error, 'odoo.clearState'); + } + })); + + context.subscriptions.push(commands.registerCommand("odoo.openChangelogView", () => { + ChangelogWebview.render(context); + })); context.subscriptions.push( commands.registerCommand('odoo.clickStatusBar', async () => { @@ -434,7 +541,7 @@ function initializeSubscriptions(context: ExtensionContext, client: LanguageClie const qpick = window.createQuickPick(); const configs: Map = context.globalState.get("Odoo.configurations"); let selectedConfiguration = null; - const currentConfig = getCurrentConfig(context); + const currentConfig = await getCurrentConfig(context); let currentConfigItem: QuickPickItem; const configMap = new Map(); const separator = { kind: QuickPickItemKind.Separator }; @@ -470,15 +577,14 @@ function initializeSubscriptions(context: ExtensionContext, client: LanguageClie selectedConfiguration = selection[0]; }); - qpick.onDidTriggerItemButton(buttonEvent => { + qpick.onDidTriggerItemButton(async (buttonEvent) => { if (buttonEvent.button.iconPath == gearIcon) { let buttonConfigId = (buttonEvent.item == currentConfigItem) ? currentConfig["id"] : configMap.get(buttonEvent.item); - try{ + try { ConfigurationWebView.render(context, Number(buttonConfigId)); - } - catch (error) { - odooOutputChannel.appendLine(error) - displayCrashMessage(context, error, odooOutputChannel, 'render.ConfigurationWebView') + } catch (error) { + global.LSCLIENT.error(error); + await displayCrashMessage(context, error, 'render.ConfigurationWebView'); } } }); @@ -489,15 +595,15 @@ function initializeSubscriptions(context: ExtensionContext, client: LanguageClie await addNewConfiguration(context); } catch (error) { - odooOutputChannel.appendLine(error) - displayCrashMessage(context, error, odooOutputChannel, 'render.ConfigurationWebView') + global.LSCLIENT.error(error) + await displayCrashMessage(context, error, 'render.ConfigurationWebView') } } else if (selectedConfiguration == disabledItem) { - changeSelectedConfig(context, -1); + await changeSelectedConfig(context, -1); } else if (selectedConfiguration && selectedConfiguration != currentConfigItem) { - changeSelectedConfig(context, configMap.get(selectedConfiguration)); + await changeSelectedConfig(context, configMap.get(selectedConfiguration)); } qpick.hide(); }); @@ -505,144 +611,37 @@ function initializeSubscriptions(context: ExtensionContext, client: LanguageClie qpick.show(); } catch (error) { - odooOutputChannel.appendLine(error) - displayCrashMessage(context, error, odooOutputChannel, 'odoo.clickStatusBar') - } - }) - ); - // Listen to changes to Configurations - context.subscriptions.push( - ConfigurationsChange.event((changes: Array | null) => { - try { - setStatusConfig(context, odooStatusBar); - if (changes && (changes.includes('odooPath') || changes.includes('addons'))) { - checkOdooPath(context); - checkAddons(context,odooOutputChannel); - if (client.diagnostics) client.diagnostics.clear(); - client.sendNotification("Odoo/configurationChanged"); - } - if (changes && changes.includes('pythonPath')) { - checkRestartPythonServer() - client.sendNotification("Odoo/configurationChanged"); - } - } - catch (error) { - odooOutputChannel.appendLine(error) - displayCrashMessage(context, error, odooOutputChannel, 'event.ConfigurationsChange') - } - }) - ); - - // Listen to changes to the selected Configuration - context.subscriptions.push( - selectedConfigurationChange.event(() => { - try { - if (getCurrentConfig(context)) { - checkRestartPythonServer() - checkOdooPath(context); - checkAddons(context, odooOutputChannel); - if (!client.isRunning()) { - client.start().then(() => { - client.sendNotification( - "Odoo/clientReady", - ); - }); - } else { - if (client.diagnostics) client.diagnostics.clear(); - client.sendNotification("Odoo/configurationChanged"); - } - } else { - if (client.isRunning()) client.stop(); - isLoading = false; - } - setStatusConfig(context, odooStatusBar); - } - catch (error) { - odooOutputChannel.appendLine(error) - displayCrashMessage(context, error, odooOutputChannel, 'event.selectedConfigurationChange') + global.LSCLIENT.error(error) + await displayCrashMessage(context, error, 'odoo.clickStatusBar') } }) ); - // COMMANDS - context.subscriptions.push( - commands.registerCommand("odoo.openWelcomeView", () => { - try { - WelcomeWebView.render(context); - } - catch (error) { - odooOutputChannel.appendLine(error) - displayCrashMessage(context, error, odooOutputChannel, 'odoo.openWelcomeView') - } - }) - ); - - context.subscriptions.push( - commands.registerCommand("odoo.clearState", () => { - try { - for (let key of context.globalState.keys()) { - odooOutputChannel.appendLine(`[INFO] Wiping ${key} from global storage.`); - context.globalState.update(key, undefined); - } - - for (let key of context.workspaceState.keys()) { - odooOutputChannel.appendLine(`[INFO] Wiping ${key} from workspace storage.`); - context.workspaceState.update(key, undefined); - } - commands.executeCommand("workbench.action.reloadWindow"); - } - catch (error) { - odooOutputChannel.appendLine(error) - displayCrashMessage(context, error, odooOutputChannel, 'odoo.clearState') - } - })); - - context.subscriptions.push(commands.registerCommand("odoo.openChangelogView", () => { - ChangelogWebview.render(context); - })); - if (context.extensionMode === ExtensionMode.Development) { - context.subscriptions.push(commands.registerCommand("odoo.testCrashMessage", () => { displayCrashMessage(context, "Test crash message", odooOutputChannel); })); + context.subscriptions.push( + commands.registerCommand( + "odoo.testCrashMessage", async () => { + await displayCrashMessage(context, "Test crash message"); + } + ) + ); } - - context.subscriptions.push( - client.onNotification("Odoo/loadingStatusUpdate", (state: String) => { - switch (state) { - case "start": - isLoading = true; - break; - case "stop": - isLoading = false; - break; - } - setStatusConfig(context, odooStatusBar); - }) - ); - context.subscriptions.push(client.onRequest("Odoo/getConfiguration", (params) => { - return getCurrentConfig(context); - })); - - context.subscriptions.push(client.onNotification("Odoo/displayCrashNotification", (params) => { - displayCrashMessage(context, params["crashInfo"], odooOutputChannel); - })); } -export function activate(context: ExtensionContext): void { - const odooOutputChannel: OutputChannel = window.createOutputChannel('Odoo', 'python'); + +export async function activate(context: ExtensionContext): Promise { try { - let pythonPath = getPythonPath(context); - let client = startLanguageServerClient(context, pythonPath, odooOutputChannel); - const config = getCurrentConfig(context); - deleteOldFiles(context, odooOutputChannel) - odooOutputChannel.appendLine('[INFO] Starting the extension.'); - - // new ConfigurationsExplorer(context); - checkOdooPath(context); - checkAddons(context, odooOutputChannel); - initializeSubscriptions(context, client, odooOutputChannel) + global.CAN_QUEUE_CONFIG_CHANGE = true; + global.DEBUG_FILE = `pygls-${new Date().toISOString().replaceAll(":","_")}.log`; + global.OUTPUT_CHANNEL = window.createOutputChannel('Odoo', 'python'); + global.LSCLIENT = await initLanguageServerClient(context, global.OUTPUT_CHANNEL); // Initialize some settings on the extension's launch if they're missing from the state. - setMissingStateVariables(context, odooOutputChannel); - validateState(context, odooOutputChannel) + setMissingStateVariables(context); + validateState(context); + + + await initStatusBar(context); + await initializeSubscriptions(context); switch (context.globalState.get('Odoo.displayWelcomeView', null)) { case null: @@ -655,38 +654,117 @@ export function activate(context: ExtensionContext): void { } // Check if the extension was updated since the last time. - if (isExtensionUpdated(context)) displayUpdatedNotification(context); + if (isExtensionUpdated(context)) await displayUpdatedNotification(context); // We update the last used version on every run. updateLastRecordedVersion(context); + const config = await getCurrentConfig(context); if (config) { - odooStatusBar.text = `Odoo (${config["name"]})` - if (checkPythonVersion(pythonPath)) { - client.start() - client.sendNotification( - "Odoo/clientReady", - ); - } else { - displayInvalidPythonError(context) - } + deleteOldFiles(context) + global.LSCLIENT.info('Starting the extension.'); + + await checkOdooPath(context); + await checkAddons(context); + + global.STATUS_BAR.text = `Odoo (${config["name"]})` + await global.LSCLIENT.start(); + await global.LSCLIENT.sendNotification( + "Odoo/clientReady", + ); } } catch (error) { - odooOutputChannel.appendLine(error) - displayCrashMessage(context, error, odooOutputChannel, 'odoo.activate') + global.LSCLIENT.error(error); + displayCrashMessage(context, error, 'odoo.activate'); + } +} + +async function waitForClientStop() { + return new Promise(resolve => { + clientStopped.event(e => { + resolve(); + }) + }); +} + +async function stopClient() { + if (global.LSCLIENT && !global.CLIENT_IS_STOPPING) { + global.LSCLIENT.info("Stopping LS Client."); + global.CLIENT_IS_STOPPING = true; + await global.LSCLIENT.stop(15000); + global.CLIENT_IS_STOPPING = false; + clientStopped.fire(null); + global.LSCLIENT.info("LS Client stopped."); + } +} + +export async function deactivate(): Promise { + if (global.LSCLIENT) { + return global.LSCLIENT.dispose(); } } -function closeClient(client: LanguageClient) { - if (client.diagnostics) client.diagnostics.clear(); - if (client.isRunning()) return client.stop().then(() => client.dispose()) - return client.dispose(); +async function getStandalonePythonPath(context: ExtensionContext) { + const config = await getCurrentConfig(context); + const pythonPath = config && config["pythonPath"] ? config["pythonPath"] : "python3"; + return pythonPath +} + +async function checkStandalonePythonVersion(context: ExtensionContext): Promise{ + const currentConfig = await getCurrentConfig(context); + let pythonPath = currentConfig["pythonPath"] + if (!pythonPath) { + OUTPUT_CHANNEL.appendLine("[INFO] pythonPath is not set, defaulting to python3."); + pythonPath = "python3" + } + + const versionString = execSync(`${pythonPath} --version`).toString().replace("Python ", "") + + const pythonVersion = semver.parse(versionString) + if (!pythonVersion || semver.lt(pythonVersion, "3.8.0")) { + window.showErrorMessage( + `You must use python 3.8 or newer. Would you like to change it?`, + "Update current configuration", + "Ignore" + ).then(selection => { + switch (selection) { + case ("Update current configuration"): + ConfigurationWebView.render(context, currentConfig.id); + break + } + }); + return false + } + return true } -export function deactivate(): Thenable | undefined { - if (!client) { - return undefined; +async function getPythonPath(context): Promise{ + let pythonPath: string; + let interpreter: IInterpreterDetails; + const config = await getCurrentConfig(context) + try { + interpreter = await getInterpreterDetails(); + } catch { + interpreter = null; } - return closeClient(client) + + //trying to use the VScode python extension + if (interpreter && global.IS_PYTHON_EXTENSION_READY !== false) { + pythonPath = interpreter.path[0] + await initializePython(context.subscriptions); + global.IS_PYTHON_EXTENSION_READY = true; + } else { + global.IS_PYTHON_EXTENSION_READY = false; + //python extension is not available switch to standalone mode + if (config){ + pythonPath = await getStandalonePythonPath(context); + await checkStandalonePythonVersion(context); + } + } + + global.OUTPUT_CHANNEL.appendLine("[INFO] Python VS code extension is ".concat(global.IS_PYTHON_EXTENSION_READY ? "ready" : "not ready")); + global.OUTPUT_CHANNEL.appendLine("[INFO] Using Python at : ".concat(pythonPath)); + + return pythonPath } diff --git a/vscode/client/global.d.ts b/vscode/client/global.d.ts new file mode 100644 index 00000000..0fd39741 --- /dev/null +++ b/vscode/client/global.d.ts @@ -0,0 +1,15 @@ +import { OutputChannel, StatusBarItem } from "vscode"; +import { + LanguageClient, +} from "vscode-languageclient/node"; + +declare global { + var LSCLIENT: LanguageClient; + var STATUS_BAR: StatusBarItem; + var OUTPUT_CHANNEL: OutputChannel; + var IS_LOADING: boolean; + var DEBUG_FILE: string; + var CLIENT_IS_STOPPING: boolean; + var CAN_QUEUE_CONFIG_CHANGE: boolean; + var IS_PYTHON_EXTENSION_READY: boolean; +} diff --git a/vscode/client/views/changelog/changelogWebview.ts b/vscode/client/views/changelog/changelogWebview.ts index 65429b3d..5ce40227 100644 --- a/vscode/client/views/changelog/changelogWebview.ts +++ b/vscode/client/views/changelog/changelogWebview.ts @@ -3,7 +3,7 @@ import * as vscode from 'vscode'; import {readFileSync} from 'fs'; import * as ejs from "ejs"; import MarkdownIt = require('markdown-it'); -import { getNonce, getUri } from "../../utils/utils"; +import { getNonce, getUri } from "../../common/utils"; const md = new MarkdownIt('commonmark'); diff --git a/vscode/client/views/configurations/configurationWebView.html b/vscode/client/views/configurations/configurationWebView.html index 9e9aca75..5cf539d2 100644 --- a/vscode/client/views/configurations/configurationWebView.html +++ b/vscode/client/views/configurations/configurationWebView.html @@ -42,11 +42,12 @@

Odoo Configuration

<% } %> - - - Python path - - + <% if (!pythonExtensionMode) { %> + + + Path to the Python binary the Language Server will run on + + Odoo Configuration action-icon > - - -

Path to the Python binary the Language Server will run on.

-
-
+
+ + +
+ <% } %> Additional addons diff --git a/vscode/client/views/configurations/configurationWebView.js b/vscode/client/views/configurations/configurationWebView.js index e3233fb1..d90c06f5 100644 --- a/vscode/client/views/configurations/configurationWebView.js +++ b/vscode/client/views/configurations/configurationWebView.js @@ -40,9 +40,11 @@ function main() { addFolderButton.addEventListener("click", addFolderClick); pathTextfield.addEventListener("vsc-change", updateVersion); pathButton.addEventListener('vsc-click', openOdooFolder); + if (pythonPathButton){ + pythonPathButton.addEventListener('vsc-click', openPythonPath); + } saveButton.addEventListener('click', saveConfig); deleteButton.addEventListener('click', deleteConfig); - pythonPathButton.addEventListener('vsc-click', openPythonPath); // Send a message to notify the extension // that the DOM is loaded and ready. @@ -52,12 +54,25 @@ function main() { } function saveConfig() { + let pythonPath = document.getElementById("config-python-path-textfield"); + if (!pythonPath){ + pythonPath=undefined + }else{ + pythonPath=pythonPath.value + } + vscode.postMessage({ command: "save_config", name: document.getElementById("config-name-textfield").value, odooPath: document.getElementById("config-path-textfield").value, addons: getAddons(), - pythonPath: document.getElementById("config-python-path-textfield").value, + pythonPath: pythonPath, + }); +} + +function openPythonPath() { + vscode.postMessage({ + command: "open_python_path" }); } @@ -80,12 +95,6 @@ function openOdooFolder() { }); } -function openPythonPath() { - vscode.postMessage({ - command: "open_python_path" - }); -} - function deleteConfig() { vscode.postMessage({ command: "delete_config" diff --git a/vscode/client/views/configurations/configurationWebView.ts b/vscode/client/views/configurations/configurationWebView.ts index 83a6a445..eb285127 100644 --- a/vscode/client/views/configurations/configurationWebView.ts +++ b/vscode/client/views/configurations/configurationWebView.ts @@ -1,6 +1,6 @@ import { Disposable, Webview, WebviewPanel, window, Uri, ViewColumn } from "vscode"; -import { getUri, getNonce } from "../../utils/utils"; -import {ConfigurationsChange} from "../../utils/events" +import { getUri, getNonce } from "../../common/utils"; +import {ConfigurationsChange} from "../../common/events" import * as ejs from "ejs"; import * as vscode from 'vscode'; import * as fs from 'fs'; @@ -129,7 +129,8 @@ export class ConfigurationWebView { config: config, cspSource: webview.cspSource, nonce: nonce, - odooVersion: configsVersion ? configsVersion[`${this.configId}`] : null + odooVersion: configsVersion ? configsVersion[`${this.configId}`] : null, + pythonExtensionMode: global.IS_PYTHON_EXTENSION_READY, }; return ejs.render(htmlFile, data); } @@ -260,6 +261,9 @@ export class ConfigurationWebView { case "delete_config": this._deleteConfig(configs); break; + case "update_version": + this._getOdooVersion(message.odooPath, webview); + break; case "open_python_path": const pythonPathOptions: vscode.OpenDialogOptions = { title: "Add Python path", @@ -279,9 +283,6 @@ export class ConfigurationWebView { } }); break; - case "update_version": - this._getOdooVersion(message.odooPath, webview); - break; } }, undefined, diff --git a/vscode/client/views/crash_report/crashReport.ts b/vscode/client/views/crash_report/crashReport.ts index dc3947e7..cfc9cde5 100644 --- a/vscode/client/views/crash_report/crashReport.ts +++ b/vscode/client/views/crash_report/crashReport.ts @@ -1,11 +1,13 @@ import { Disposable, Webview, WebviewPanel, window, Uri } from "vscode"; -import { getUri, getNonce, getPythonVersion, getCurrentConfig } from "../../utils/utils"; +import { getUri, getNonce, getCurrentConfig } from "../../common/utils"; +import { getPythonVersion } from "../../common/python"; import axios from 'axios'; import * as ejs from "ejs"; import * as vscode from 'vscode'; import * as fs from 'fs'; import * as crypto from 'crypto'; + export class CrashReportWebView { public static panels: Map | undefined; public static readonly viewType = 'odooCrashReport'; @@ -127,12 +129,13 @@ export class CrashReportWebView { * @param context A reference to the extension context */ private _setWebviewMessageListener(webview: Webview) { - webview.onDidReceiveMessage((message: any) => { + webview.onDidReceiveMessage(async (message: any) => { const command = message.command; switch (command) { case "send_report": - const config = getCurrentConfig(this._context); + const config = await getCurrentConfig(this._context); + const version = await getPythonVersion(); let configString = ""; if (config) { configString += `Path: ${config.odooPath}\n`; @@ -149,7 +152,7 @@ export class CrashReportWebView { error: this._error, additional_info: message.additional_info, version: this._context.extension.packageJSON.version, - python_version: getPythonVersion(this._context), + python_version: `${version.major}.${version.minor}.${version.micro}`, configuration: configString, command: this._command, } diff --git a/vscode/client/views/welcome/welcomeWebView.ts b/vscode/client/views/welcome/welcomeWebView.ts index fc2328af..81212e6a 100644 --- a/vscode/client/views/welcome/welcomeWebView.ts +++ b/vscode/client/views/welcome/welcomeWebView.ts @@ -1,5 +1,5 @@ import { Disposable, Webview, WebviewPanel, window, Uri, ViewColumn } from "vscode"; -import { getNonce, getUri } from "../../utils/utils"; +import { getNonce, getUri } from "../../common/utils"; import * as vscode from 'vscode'; import * as path from 'path'; import * as fs from 'fs'; diff --git a/vscode/package.json b/vscode/package.json index 7810919c..e98eb6ff 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -48,20 +48,27 @@ "title": "Open the Changelog page", "category": "Odoo" }, - { - "command": "odoo.testCrashMessage", - "title": "Open the crash notification", - "category": "Odoo", - "when": "inDebugMode" - }, { "command": "odoo.clearState", "title": "Wipe the extension's state storage (!!!THIS ACTION IS IRREVERSIBLE!!!)", "category": "Odoo" + }, + { + "command": "odoo.testCrashMessage", + "title": "Open the crash notification", + "category": "Odoo" } ], "views": {}, "menus": { + "commandPalette": [ + { + "command": "odoo.testCrashMessage", + "title": "Open the crash notification", + "category": "Odoo", + "when": "odoo.showCrashNotificationCommand" + } + ], "view/title": [ { "command": "odoo.addConfiguration", @@ -146,9 +153,9 @@ "@types/ejs": "^3.1.2", "@types/markdown-it": "^13.0.1", "@types/node": "^16.11.6", + "@types/semver": "^7.5.2", "@types/vscode": "^1.78.0", "@types/vscode-webview": "^1.57.0", - "@types/semver": "^7.5.2", "@typescript-eslint/eslint-plugin": "^5.3.0", "@typescript-eslint/parser": "^5.3.0", "esbuild": "^0.19.2", @@ -158,6 +165,7 @@ "dependencies": { "@bendera/vscode-webview-elements": "^0.14.0", "@vscode/codicons": "^0.0.33", + "@vscode/python-extension": "^1.0.5", "@vscode/webview-ui-toolkit": "^1.2.2", "axios": "^1.4.0", "ejs": "^3.1.9",