From c6e0bdf9788a2b273abdece7d79202a228f106e7 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Thu, 20 Jun 2024 09:48:33 +0200 Subject: [PATCH] fix: syntax highlighting not working (#52) Consistently use the file extension `.ttsl` so syntax highlighting works. --------- Co-authored-by: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com> --- packages/ttsl-lang/langium-config.json | 8 +- .../grammar/{safe-ds.langium => ttsl.langium} | 2 +- .../src/language/helpers/nodeProperties.ts | 4 +- packages/ttsl-lang/src/language/index.ts | 5 +- .../ttsl-lang/src/language/runner/messages.ts | 281 --------- .../src/language/runner/safe-ds-runner.ts | 555 ------------------ .../ttsl-lang/src/language/safe-ds-module.ts | 22 +- .../scoping/safe-ds-scope-computation.ts | 2 +- .../language/validation/safe-ds-validator.ts | 4 +- .../workspace/safe-ds-package-manager.ts | 4 +- .../workspace/safe-ds-settings-provider.ts | 6 +- .../tests/language/runner/messages.test.ts | 54 -- .../language/runner/safe-ds-runner.test.ts | 71 --- packages/ttsl-vscode/package.json | 40 +- .../ttsl-vscode/src/extension/mainClient.ts | 206 +------ 15 files changed, 32 insertions(+), 1232 deletions(-) rename packages/ttsl-lang/src/language/grammar/{safe-ds.langium => ttsl.langium} (99%) delete mode 100644 packages/ttsl-lang/src/language/runner/messages.ts delete mode 100644 packages/ttsl-lang/src/language/runner/safe-ds-runner.ts delete mode 100644 packages/ttsl-lang/tests/language/runner/messages.test.ts delete mode 100644 packages/ttsl-lang/tests/language/runner/safe-ds-runner.test.ts diff --git a/packages/ttsl-lang/langium-config.json b/packages/ttsl-lang/langium-config.json index 6ddc905e..761f4de7 100644 --- a/packages/ttsl-lang/langium-config.json +++ b/packages/ttsl-lang/langium-config.json @@ -1,10 +1,10 @@ { - "projectName": "SafeDs", + "projectName": "TTSL", "languages": [ { - "id": "safe-ds", - "grammar": "src/language/grammar/safe-ds.langium", - "fileExtensions": [".tsl", ".tsltest"] + "id": "ttsl", + "grammar": "src/language/grammar/ttsl.langium", + "fileExtensions": [".ttsl"] } ], "out": "src/language/generated" diff --git a/packages/ttsl-lang/src/language/grammar/safe-ds.langium b/packages/ttsl-lang/src/language/grammar/ttsl.langium similarity index 99% rename from packages/ttsl-lang/src/language/grammar/safe-ds.langium rename to packages/ttsl-lang/src/language/grammar/ttsl.langium index 79698e7a..e62ca309 100644 --- a/packages/ttsl-lang/src/language/grammar/safe-ds.langium +++ b/packages/ttsl-lang/src/language/grammar/ttsl.langium @@ -1,4 +1,4 @@ -grammar TSL +grammar TTSL // ----------------------------------------------------------------------------- // Base interfaces diff --git a/packages/ttsl-lang/src/language/helpers/nodeProperties.ts b/packages/ttsl-lang/src/language/helpers/nodeProperties.ts index 8c48bbc5..eab7f6e1 100644 --- a/packages/ttsl-lang/src/language/helpers/nodeProperties.ts +++ b/packages/ttsl-lang/src/language/helpers/nodeProperties.ts @@ -83,8 +83,8 @@ export const hasAnnotationCallOf = ( }); }; -export const isInternal = (node: TslDeclaration | undefined): boolean => { - return isTslSegment(node) && node.visibility === 'internal'; +export const isPackagePrivate = (node: TslDeclaration | undefined): boolean => { + return isTslSegment(node) && (node.visibility?.isPackageprivate ?? false); }; export namespace Argument { diff --git a/packages/ttsl-lang/src/language/index.ts b/packages/ttsl-lang/src/language/index.ts index c4ad95b2..570c8b91 100644 --- a/packages/ttsl-lang/src/language/index.ts +++ b/packages/ttsl-lang/src/language/index.ts @@ -6,7 +6,7 @@ export { createSafeDsServices } from './safe-ds-module.js'; export { startLanguageServer } from './main.js'; // Language Metadata -export { SafeDsLanguageMetaData } from './generated/module.js'; +export { TTSLLanguageMetaData } from './generated/module.js'; // AST export * as ast from './generated/ast.js'; @@ -14,6 +14,3 @@ export * from './helpers/nodeProperties.js'; // Location export { locationToString, positionToString, rangeToString } from '../helpers/locations.js'; - -// Messages -export * as messages from './runner/messages.js'; diff --git a/packages/ttsl-lang/src/language/runner/messages.ts b/packages/ttsl-lang/src/language/runner/messages.ts deleted file mode 100644 index 3ce8cb47..00000000 --- a/packages/ttsl-lang/src/language/runner/messages.ts +++ /dev/null @@ -1,281 +0,0 @@ -/** - * Any message that can be sent or received by the runner. - * - * The type field identifies the type of the message. - * - * The id field is a unique identifier to track messages to their origin. - * A program message contains the id. A response message containing an error, progress or a placeholder then contains the same id. - */ -export type PythonServerMessage = - | ProgramMessage - | PlaceholderQueryMessage - | PlaceholderTypeMessage - | PlaceholderValueMessage - | RuntimeErrorMessage - | RuntimeProgressMessage - | ShutdownMessage; - -export type RuntimeProgress = 'done'; - -// Extension to Runner -/** - * Message that contains a fully executable compiled Safe-DS pipeline. - */ -export interface ProgramMessage { - type: 'program'; - id: string; - data: ProgramPackageMap; -} - -/** - * Contains code and the description of the main entry point of a pipeline. - */ -export interface ProgramPackageMap { - code: ProgramCodeMap; - main: ProgramMainInformation; -} - -/** - * Contains python modules grouped by a virtual directory structure. The key is a path, directories are separated by '.'. - */ -export interface ProgramCodeMap { - [key: string]: ProgramModuleMap; -} - -/** - * Contains python module code identified by the module name. - */ -export interface ProgramModuleMap { - [key: string]: string; -} - -/** - * Contains execution information about a pipeline. - */ -export interface ProgramMainInformation { - /** - * The path to the current module. - */ - modulepath: string; - - /** - * The current module name. - */ - module: string; - - /** - * The pipeline name. - */ - pipeline: string; -} - -// Extension to Runner -/** - * Message that contains a request to send back the value of a specified placeholder - */ -export interface PlaceholderQueryMessage { - type: 'placeholder_query'; - id: string; - data: PlaceholderQuery; -} - -/** - * A query on a placeholder value. - */ -export interface PlaceholderQuery { - /** - * The name of the requested placeholder. - */ - name: string; - - /** - * Optional windowing information to request a subset of the available data. - */ - window: PlaceholderQueryWindow; -} - -/** - * Windowing information for the placeholder query. - */ -export interface PlaceholderQueryWindow { - /** - * The offset of the requested data. - */ - begin?: number; - - /** - * The size of the requested data. - */ - size?: number; -} - -// Runner to Extension -/** - * Message that contains information about a calculated placeholder. - */ -export interface PlaceholderTypeMessage { - type: 'placeholder_type'; - id: string; - data: PlaceholderDescription; -} - -/** - * Contains the description of a calculated placeholder. - */ -export interface PlaceholderDescription { - /** - * Name of the calculated placeholder. - */ - name: string; - - /** - * Type of the calculated placeholder - */ - type: string; -} - -/** - * Message that contains the value of a calculated placeholder. - */ -export interface PlaceholderValueMessage { - type: 'placeholder_value'; - id: string; - data: PlaceholderValue; -} - -/** - * Contains the description and the value of a calculated placeholder. - */ -export interface PlaceholderValue { - /** - * Name of the calculated placeholder. - */ - name: string; - - /** - * Type of the calculated placeholder. - */ - type: string; - - /** - * Actual value of the calculated placeholder. - */ - value: string; - - /** - * Optional windowing information when only a subset of the data was requested. This may be different from the requested bounds. - */ - window?: PlaceholderValueWindow; -} - -/** - * Windowing information for a placeholder value response. - */ -export interface PlaceholderValueWindow { - /** - * Index offset of the requested data subset. - */ - begin: number; - - /** - * Size of the requested data subset. - */ - size: number; - - /** - * Max. amount of elements available. - */ - max: number; -} - -// Runner to Extension -/** - * Message that contains information about a runtime error that occurred during execution. - */ -export interface RuntimeErrorMessage { - type: 'runtime_error'; - id: string; - data: RuntimeErrorDescription; -} - -/** - * Error description for runtime errors. - */ -export interface RuntimeErrorDescription { - /** - * Error Message - */ - message: string; - - /** - * Array of stackframes at the moment of raising the error. - */ - backtrace: RuntimeErrorBacktraceFrame[]; -} - -/** - * Contains debugging information about a stackframe. - */ -export interface RuntimeErrorBacktraceFrame { - /** - * Python module name (or file name). - */ - file: string; - - /** - * Line number where the error occurred. - */ - line: number; -} - -// Runner to Extension -/** - * Message that contains information about the current execution progress. - * Field data currently supports on of the following: 'done' - * - * A progress value of 'done' means that the pipeline execution completed. - */ -export interface RuntimeProgressMessage { - type: 'runtime_progress'; - id: string; - data: RuntimeProgress; -} - -export const createProgramMessage = function (id: string, data: ProgramPackageMap): PythonServerMessage { - return { type: 'program', id, data }; -}; - -export const createPlaceholderQueryMessage = function ( - id: string, - placeholderName: string, - windowBegin: number | undefined = undefined, - windowSize: number | undefined = undefined, -): PythonServerMessage { - return { - type: 'placeholder_query', - id, - data: { - name: placeholderName, - window: { - begin: !windowBegin ? undefined : Math.round(windowBegin), - size: !windowSize ? undefined : Math.round(windowSize), - }, - }, - }; -}; - -// Extension to Runner -/** - * Message that instructs the runner to shut itself down as soon as possible. - * - * There will be no response to this message, data and id fields are therefore empty. - */ -export interface ShutdownMessage { - type: 'shutdown'; - id: ''; - data: ''; -} - -export const createShutdownMessage = function (): PythonServerMessage { - return { type: 'shutdown', id: '', data: '' }; -}; diff --git a/packages/ttsl-lang/src/language/runner/safe-ds-runner.ts b/packages/ttsl-lang/src/language/runner/safe-ds-runner.ts deleted file mode 100644 index c7415333..00000000 --- a/packages/ttsl-lang/src/language/runner/safe-ds-runner.ts +++ /dev/null @@ -1,555 +0,0 @@ -import child_process from 'child_process'; -import net from 'net'; -import WebSocket from 'ws'; -import { SafeDsServices } from '../safe-ds-module.js'; -import { LangiumDocument, URI } from 'langium'; -import path from 'path'; -import { - createProgramMessage, - createShutdownMessage, - ProgramCodeMap, - PythonServerMessage, - RuntimeErrorBacktraceFrame, -} from './messages.js'; -import { BasicSourceMapConsumer, SourceMapConsumer } from 'source-map'; -import treeKill from 'tree-kill'; -import { SafeDsAnnotations } from '../builtins/safe-ds-annotations.js'; -import { SafeDsPythonGenerator } from '../generation/safe-ds-python-generator.js'; -import { isTslModule } from '../generated/ast.js'; -import semver from 'semver'; - -// Most of the functionality cannot be tested automatically as a functioning runner setup would always be required - -const LOWEST_SUPPORTED_VERSION = '0.7.0'; -const LOWEST_UNSUPPORTED_VERSION = '0.8.0'; -const npmVersionRange = `>=${LOWEST_SUPPORTED_VERSION} <${LOWEST_UNSUPPORTED_VERSION}`; -const pipVersionRange = `>=${LOWEST_SUPPORTED_VERSION},<${LOWEST_UNSUPPORTED_VERSION}`; - -export class SafeDsRunner { - private readonly annotations: SafeDsAnnotations; - private readonly generator: SafeDsPythonGenerator; - - private logging: RunnerLoggingOutput; - private runnerCommand: string = 'safe-ds-runner'; - private runnerProcess: child_process.ChildProcessWithoutNullStreams | undefined = undefined; - private port: number | undefined = undefined; - private acceptsConnections: boolean = false; - private serverConnection: WebSocket | undefined = undefined; - private messageCallbacks: Map void)[]> = new Map< - PythonServerMessage['type'], - ((message: PythonServerMessage) => void)[] - >(); - - /** - * Map that contains information about an execution keyed by the execution id. - */ - private executionInformation: Map = new Map< - string, - PipelineExecutionInformation - >(); - - constructor(services: SafeDsServices) { - this.annotations = services.builtins.Annotations; - this.generator = services.generation.PythonGenerator; - this.logging = { - outputError(_value: string) {}, - outputInfo(_value: string) {}, - displayError(_value: string) {}, - }; - } - - /** - * Change the command to start the runner process. This will not cause the runner process to restart, if it is already running. - * - * @param command New Runner Command. - */ - /* c8 ignore start */ - public updateRunnerCommand(command: string | undefined): void { - if (command) { - this.runnerCommand = command; - } - } - - /** - * Change the output functions for runner logging and error information to those provided. - * - * @param logging New Runner output functions. - */ - public updateRunnerLogging(logging: RunnerLoggingOutput): void { - this.logging = logging; - } - - /** - * Start the python server on the next usable port, starting at 5000. - * Uses the 'safe-ds.runner.command' setting to execute the process. - */ - public async startPythonServer(): Promise { - this.acceptsConnections = false; - const runnerCommandParts = this.runnerCommand.split(/\s/u); - const runnerCommand = runnerCommandParts.shift()!; // After shift, only the actual args are left - // Test if the python server can actually be started - try { - const pythonServerTest = child_process.spawn(runnerCommand, [...runnerCommandParts, '-V']); - const versionString = await this.getPythonServerVersion(pythonServerTest); - if (!semver.satisfies(versionString, npmVersionRange)) { - this.logging.outputError( - `Installed runner version ${versionString} does not meet requirements: ${pipVersionRange}`, - ); - this.logging.displayError( - `The installed runner version ${versionString} is not compatible with this version of the extension. The installed version should match these requirements: ${pipVersionRange}. Please update to a matching version.`, - ); - return; - } else { - this.logging.outputInfo(`Using safe-ds-runner version: ${versionString}`); - } - } catch (error) { - this.logging.outputError(`Could not start runner: ${error instanceof Error ? error.message : error}`); - this.logging.displayError( - `The runner process could not be started: ${error instanceof Error ? error.message : error}`, - ); - return; - } - // Start the runner at the specified port - this.port = await this.findFirstFreePort(5000); - this.logging.outputInfo(`Trying to use port ${this.port} to start python server...`); - this.logging.outputInfo(`Using command '${this.runnerCommand}' to start python server...`); - - const runnerArgs = [...runnerCommandParts, 'start', '--port', String(this.port)]; - this.logging.outputInfo(`Running ${runnerCommand}; Args: ${runnerArgs.join(' ')}`); - this.runnerProcess = child_process.spawn(runnerCommand, runnerArgs); - this.manageRunnerSubprocessOutputIO(); - try { - await this.connectToWebSocket(); - } catch (error) { - await this.stopPythonServer(); - return; - } - this.logging.outputInfo('Started python server successfully'); - } - - /** - * Stop the python server process, if any currently exist. This will first try a graceful shutdown. - * If that fails, the whole process tree (starting at the child process spawned by startPythonServer) will get killed. - */ - async stopPythonServer(): Promise { - this.logging.outputInfo('Stopping python server...'); - if (this.runnerProcess !== undefined) { - if ((this.acceptsConnections && !(await this.requestGracefulShutdown(2500))) || !this.acceptsConnections) { - this.logging.outputInfo(`Tree-killing python server process ${this.runnerProcess.pid}...`); - const pid = this.runnerProcess.pid!; - // Wait for tree-kill to finish killing the tree - await new Promise((resolve, _reject) => { - treeKill(pid, (error) => { - resolve(); - if (error) { - this.logging.outputError(`Error while killing runner process tree: ${error}`); - } - }); - }); - // If tree-kill did not work, we don't have any more options - } - } - this.runnerProcess = undefined; - this.acceptsConnections = false; - } - - private async requestGracefulShutdown(maxTimeoutMs: number): Promise { - this.logging.outputInfo('Trying graceful shutdown...'); - this.sendMessageToPythonServer(createShutdownMessage()); - return new Promise((resolve, _reject) => { - this.runnerProcess?.on('close', () => resolve(true)); - setTimeout(() => { - if (this.runnerProcess === undefined) { - resolve(true); - } else { - resolve(false); - } - }, maxTimeoutMs); - }); - } - - /** - * @return True if the python server was started and the websocket connection was established, false otherwise. - */ - public isPythonServerAvailable(): boolean { - return this.acceptsConnections; - } - - /** - * Register a callback to execute when a message from the python server arrives. - * - * @param callback Callback to execute - * @param messageType Message type to register the callback for. - */ - public addMessageCallback( - callback: (message: Extract) => void, - messageType: M, - ): void { - if (!this.messageCallbacks.has(messageType)) { - this.messageCallbacks.set(messageType, []); - } - this.messageCallbacks.get(messageType)!.push(<(message: PythonServerMessage) => void>callback); - } - - /** - * Remove a previously registered callback from being called when a message from the python server arrives. - * - * @param callback Callback to remove - * @param messageType Message type the callback was registered for. - */ - public removeMessageCallback( - callback: (message: Extract) => void, - messageType: M, - ): void { - if (!this.messageCallbacks.has(messageType)) { - return; - } - this.messageCallbacks.set( - messageType, - this.messageCallbacks.get(messageType)!.filter((storedCallback) => storedCallback !== callback), - ); - } - - /** - * Get information about a pipeline execution. - * - * @param pipelineId Unique id that identifies a pipeline execution - * @return Execution context assigned to the provided id. - */ - public getExecutionContext(pipelineId: string): PipelineExecutionInformation | undefined { - return this.executionInformation.get(pipelineId); - } - - /** - * Remove information from a pipeline execution, when it is no longer needed. - * - * @param pipelineId Unique id that identifies a pipeline execution - */ - public dropPipelineExecutionContext(pipelineId: string) { - this.executionInformation.delete(pipelineId); - } - - /** - * Remove information from all previous pipeline executions. - */ - public dropAllPipelineExecutionContexts() { - this.executionInformation.clear(); - } - - /** - * Map a stack frame from python to Safe-DS. - * Uses generated sourcemaps to do this. - * If such a mapping does not exist, this function returns undefined. - * - * @param executionId Id that uniquely identifies the execution that produced this stack frame - * @param frame Stack frame from the python execution - */ - public async tryMapToSafeDSSource( - executionId: string, - frame: RuntimeErrorBacktraceFrame | undefined, - ): Promise { - if (!frame) { - return undefined; - } - if (!this.executionInformation.has(executionId)) { - return undefined; - } - const execInfo = this.executionInformation.get(executionId)!; - let sourceMapKeys = Array.from(execInfo.generatedSource.keys() || []).filter((value) => - value.endsWith(`${frame.file}.py.map`), - ); - if (sourceMapKeys.length === 0) { - return undefined; - } - let sourceMapKey = sourceMapKeys[0]!; - if (!execInfo.sourceMappings.has(sourceMapKey)) { - const sourceMapObject = JSON.parse(execInfo.generatedSource.get(sourceMapKey)!); - sourceMapObject.sourcesContent = [execInfo.source]; - const consumer = await new SourceMapConsumer(sourceMapObject); - execInfo.sourceMappings.set(sourceMapKey, consumer); - } - const outputPosition = execInfo.sourceMappings.get(sourceMapKey)!.originalPositionFor({ - line: Number(frame.line), - column: 0, - bias: SourceMapConsumer.LEAST_UPPER_BOUND, - }); - return { file: outputPosition.source || '', line: outputPosition.line || 0 }; - } - - /** - * Execute a Safe-DS pipeline on the python runner. - * If a valid target placeholder is provided, the pipeline is only executed partially, to calculate the result of the placeholder. - * - * @param id A unique id that is used in further communication with this pipeline. - * @param pipelineDocument Document containing the main Safe-DS pipeline to execute. - * @param pipelineName Name of the pipeline that should be run - * @param targetPlaceholder The name of the target placeholder, used to do partial execution. If no value or undefined is provided, the entire pipeline is run. - */ - public async executePipeline( - id: string, - pipelineDocument: LangiumDocument, - pipelineName: string, - targetPlaceholder: string | undefined = undefined, - ) { - if (!this.isPythonServerAvailable()) { - await this.stopPythonServer(); - await this.startPythonServer(); - // just fail silently, startPythonServer should already display an error - if (!this.isPythonServerAvailable()) { - return; - } - } - const node = pipelineDocument.parseResult.value; - if (!isTslModule(node)) { - return; - } - // Pipeline / Module name handling - const mainPythonModuleName = this.annotations.getPythonModule(node); - const mainPackage = mainPythonModuleName === undefined ? node.name.split('.') : [mainPythonModuleName]; - const mainModuleName = this.getMainModuleName(pipelineDocument); - // Code generation - const [codeMap, lastGeneratedSources] = this.generateCodeForRunner(pipelineDocument, targetPlaceholder); - // Store information about the run - this.executionInformation.set(id, { - generatedSource: lastGeneratedSources, - sourceMappings: new Map(), - path: pipelineDocument.uri.fsPath, - source: pipelineDocument.textDocument.getText(), - calculatedPlaceholders: new Map(), - }); - // Code execution - this.sendMessageToPythonServer( - createProgramMessage(id, { - code: codeMap, - main: { - modulepath: mainPackage.join('.'), - module: mainModuleName, - pipeline: pipelineName, - }, - }), - ); - } - /* c8 ignore stop */ - - public generateCodeForRunner( - pipelineDocument: LangiumDocument, - targetPlaceholder: string | undefined, - ): [ProgramCodeMap, Map] { - const rootGenerationDir = path.parse(pipelineDocument.uri.fsPath).dir; - const generatedDocuments = this.generator.generate(pipelineDocument, { - destination: URI.file(rootGenerationDir), // actual directory of main module file - createSourceMaps: true, - targetPlaceholder, - disableRunnerIntegration: false, - }); - const lastGeneratedSources = new Map(); - let codeMap: ProgramCodeMap = {}; - for (const generatedDocument of generatedDocuments) { - const fsPath = URI.parse(generatedDocument.uri).fsPath; - const workspaceRelativeFilePath = path.relative(rootGenerationDir, path.dirname(fsPath)); - const TslFileName = path.basename(fsPath); - const TslNoExtFilename = - path.extname(TslFileName).length > 0 - ? TslFileName.substring(0, TslFileName.length - path.extname(TslFileName).length) - : /* c8 ignore next */ - TslFileName; - // Put code in map for further use in the extension (e.g. to remap errors) - lastGeneratedSources.set( - path.join(workspaceRelativeFilePath, TslFileName).replaceAll('\\', '/'), - generatedDocument.getText(), - ); - // Check for sourcemaps after they are already added to the pipeline context - // This needs to happen after lastGeneratedSources.set, as errors would not get mapped otherwise - if (fsPath.endsWith('.map')) { - // exclude sourcemaps from sending to runner - continue; - } - let modulePath = workspaceRelativeFilePath.replaceAll('/', '.').replaceAll('\\', '.'); - if (!codeMap.hasOwnProperty(modulePath)) { - codeMap[modulePath] = {}; - } - // Put code in object for runner - codeMap[modulePath]![TslNoExtFilename] = generatedDocument.getText(); - } - return [codeMap, lastGeneratedSources]; - } - - public getMainModuleName(pipelineDocument: LangiumDocument): string { - if (pipelineDocument.uri.fsPath.endsWith('.Tslpipe')) { - return this.generator.sanitizeModuleNameForPython(path.basename(pipelineDocument.uri.fsPath, '.Tslpipe')); - } else if (pipelineDocument.uri.fsPath.endsWith('.Tsltest')) { - return this.generator.sanitizeModuleNameForPython(path.basename(pipelineDocument.uri.fsPath, '.Tsltest')); - } else { - return this.generator.sanitizeModuleNameForPython(path.basename(pipelineDocument.uri.fsPath)); - } - } - - /** - * Send a message to the python server using the websocket connection. - * - * @param message Message to be sent to the python server. This message should be serializable to JSON. - */ - /* c8 ignore start */ - public sendMessageToPythonServer(message: PythonServerMessage): void { - const messageString = JSON.stringify(message); - this.logging.outputInfo(`Sending message to python server: ${messageString}`); - this.serverConnection!.send(messageString); - } - - private async getPythonServerVersion(process: child_process.ChildProcessWithoutNullStreams) { - process.stderr.on('data', (data: Buffer) => { - this.logging.outputInfo(`[Runner-Err] ${data.toString().trim()}`); - }); - return new Promise((resolve, reject) => { - process.stdout.on('data', (data: Buffer) => { - const version = data.toString().trim().split(/\s/u)[1]; - if (version !== undefined) { - resolve(version); - } - }); - process.on('close', (code) => { - reject(new Error(`The subprocess shut down: ${code}`)); - }); - process.on('error', (err) => { - reject(new Error(`The subprocess could not be started (${err.message})`)); - }); - }); - } - - private manageRunnerSubprocessOutputIO() { - if (!this.runnerProcess) { - return; - } - this.runnerProcess.stdout.on('data', (data: Buffer) => { - this.logging.outputInfo(`[Runner-Out] ${data.toString().trim()}`); - }); - this.runnerProcess.stderr.on('data', (data: Buffer) => { - this.logging.outputInfo(`[Runner-Err] ${data.toString().trim()}`); - }); - this.runnerProcess.on('close', (code) => { - this.logging.outputInfo(`[Runner] Exited: ${code}`); - // when the server shuts down, no connections will be accepted - this.acceptsConnections = false; - this.runnerProcess = undefined; - }); - } - - private async connectToWebSocket(): Promise { - const timeoutMs = 200; - const maxConnectionTries = 8; - let currentTry = 0; - // Attach WS - return new Promise((resolve, reject) => { - const tryConnect = () => { - this.serverConnection = new WebSocket(`ws://127.0.0.1:${this.port}/WSMain`, { - handshakeTimeout: 10 * 1000, - }); - this.serverConnection.onopen = (event) => { - this.acceptsConnections = true; - this.logging.outputInfo(`[Runner] Now accepting connections: ${event.type}`); - resolve(); - }; - this.serverConnection.onerror = (event) => { - currentTry += 1; - if (event.message.includes('ECONNREFUSED')) { - if (currentTry > maxConnectionTries) { - this.logging.outputInfo( - '[Runner] Max retries reached. No further attempt at connecting is made.', - ); - } else { - this.logging.outputInfo(`[Runner] Server is not yet up. Retrying...`); - setTimeout(tryConnect, timeoutMs * (2 ** currentTry - 1)); // use exponential backoff - return; - } - } - this.logging.outputError( - `[Runner] An error occurred: ${event.message} (${event.type}) {${event.error}}`, - ); - if (this.isPythonServerAvailable()) { - return; - } - reject(); - }; - this.serverConnection.onmessage = (event) => { - if (typeof event.data !== 'string') { - this.logging.outputInfo( - `[Runner] Message received: (${event.type}, ${typeof event.data}) ${event.data}`, - ); - return; - } - this.logging.outputInfo( - `[Runner] Message received: '${ - event.data.length > 128 ? event.data.substring(0, 128) + '' : event.data - }'`, - ); - const pythonServerMessage: PythonServerMessage = JSON.parse(event.data); - if (!this.messageCallbacks.has(pythonServerMessage.type)) { - this.logging.outputInfo(`[Runner] Message type '${pythonServerMessage.type}' is not handled`); - return; - } - for (const callback of this.messageCallbacks.get(pythonServerMessage.type)!) { - callback(pythonServerMessage); - } - }; - this.serverConnection.onclose = (_event) => { - if (this.isPythonServerAvailable()) { - // The connection was interrupted - this.acceptsConnections = false; - this.logging.outputError('[Runner] Connection was unexpectedly closed'); - } - }; - }; - tryConnect(); - }); - } - /* c8 ignore stop */ - - public async findFirstFreePort(startPort: number): Promise { - return new Promise((resolve, reject) => { - let port = startPort; - - const tryNextPort = function () { - const server = net.createServer(); - server.on('error', (err: any) => { - if (err.code === 'EADDRINUSE') { - port++; - tryNextPort(); // Port is occupied, try the next one. - } else { - /* c8 ignore next 2 */ - reject('Unknown error'); // An unexpected error occurred - } - }); - server.listen(port, '127.0.0.1', () => { - server.once('close', () => { - resolve(port); // Port is free, resolve with the current port number. - }); - server.close(); // Immediately close the server after it's opened. - }); - }; - tryNextPort(); - }); - } -} - -/** - * Runner Logging interface - */ -export interface RunnerLoggingOutput { - outputInfo: (value: string) => void; - outputError: (value: string) => void; - displayError: (value: string) => void; -} - -/** - * Context containing information about the execution of a pipeline. - */ -export interface PipelineExecutionInformation { - source: string; - generatedSource: Map; - sourceMappings: Map; - path: string; - /** - * Maps placeholder name to placeholder type - */ - calculatedPlaceholders: Map; -} diff --git a/packages/ttsl-lang/src/language/safe-ds-module.ts b/packages/ttsl-lang/src/language/safe-ds-module.ts index c45d65f9..747bc104 100644 --- a/packages/ttsl-lang/src/language/safe-ds-module.ts +++ b/packages/ttsl-lang/src/language/safe-ds-module.ts @@ -13,7 +13,7 @@ import { SafeDsEnums, SafeDsImpurityReasons } from './builtins/safe-ds-enums.js' import { SafeDsCommentProvider } from './documentation/safe-ds-comment-provider.js'; import { SafeDsDocumentationProvider } from './documentation/safe-ds-documentation-provider.js'; import { SafeDsCallGraphComputer } from './flow/safe-ds-call-graph-computer.js'; -import { SafeDsGeneratedModule, SafeDsGeneratedSharedModule } from './generated/module.js'; +import { TTSLGeneratedModule, TTSLGeneratedSharedModule } from './generated/module.js'; import { SafeDsPythonGenerator } from './generation/safe-ds-python-generator.js'; import { SafeDsValueConverter } from './grammar/safe-ds-value-converter.js'; import { SafeDsNodeMapper } from './helpers/safe-ds-node-mapper.js'; @@ -40,7 +40,6 @@ import { SafeDsWorkspaceManager } from './workspace/safe-ds-workspace-manager.js import { SafeDsPurityComputer } from './purity/safe-ds-purity-computer.js'; import { SafeDsSettingsProvider } from './workspace/safe-ds-settings-provider.js'; import { SafeDsRenameProvider } from './lsp/safe-ds-rename-provider.js'; -import { SafeDsRunner } from './runner/safe-ds-runner.js'; import { SafeDsTypeFactory } from './typing/safe-ds-type-factory.js'; /** @@ -82,9 +81,6 @@ export type SafeDsAddedServices = { PackageManager: SafeDsPackageManager; SettingsProvider: SafeDsSettingsProvider; }; - runtime: { - Runner: SafeDsRunner; - }; }; /** @@ -153,9 +149,6 @@ export const SafeDsModule: Module new SafeDsPackageManager(services), SettingsProvider: (services) => new SafeDsSettingsProvider(services), }, - runtime: { - Runner: (services) => new SafeDsRunner(services), - }, }; export type SafeDsSharedServices = LangiumSharedServices; @@ -193,8 +186,8 @@ export const createSafeDsServices = async function ( shared: LangiumSharedServices; SafeDs: SafeDsServices; }> { - const shared = inject(createDefaultSharedModule(context), SafeDsGeneratedSharedModule, SafeDsSharedModule); - const SafeDs = inject(createDefaultModule({ shared }), SafeDsGeneratedModule, SafeDsModule); + const shared = inject(createDefaultSharedModule(context), TTSLGeneratedSharedModule, SafeDsSharedModule); + const SafeDs = inject(createDefaultModule({ shared }), TTSLGeneratedModule, SafeDsModule); shared.ServiceRegistry.register(SafeDs); registerValidationChecks(SafeDs); @@ -208,10 +201,6 @@ export const createSafeDsServices = async function ( if (!options?.omitBuiltins) { await shared.workspace.WorkspaceManager.initializeWorkspace([]); } - if (options?.runnerCommand) { - /* c8 ignore next 2 */ - SafeDs.runtime.Runner.updateRunnerCommand(options?.runnerCommand); - } return { shared, SafeDs }; }; @@ -224,9 +213,4 @@ export interface ModuleOptions { * By default, builtins are loaded into the workspace. If this option is set to true, builtins are omitted. */ omitBuiltins?: boolean; - - /** - * Command to start the runner. - */ - runnerCommand?: string; } diff --git a/packages/ttsl-lang/src/language/scoping/safe-ds-scope-computation.ts b/packages/ttsl-lang/src/language/scoping/safe-ds-scope-computation.ts index 57c60a52..ac863bfa 100644 --- a/packages/ttsl-lang/src/language/scoping/safe-ds-scope-computation.ts +++ b/packages/ttsl-lang/src/language/scoping/safe-ds-scope-computation.ts @@ -30,7 +30,7 @@ import { export class SafeDsScopeComputation extends DefaultScopeComputation { protected override exportNode(node: AstNode, exports: AstNodeDescription[], document: LangiumDocument): void { // Modules, pipelines, and private segments cannot be referenced from other documents - if (isTslModule(node) || isTslPipeline(node) || (isTslSegment(node) && node.visibility === 'private')) { + if (isTslModule(node) || isTslPipeline(node) || (isTslSegment(node) && (node.visibility?.isPrivate ?? false))) { return; } diff --git a/packages/ttsl-lang/src/language/validation/safe-ds-validator.ts b/packages/ttsl-lang/src/language/validation/safe-ds-validator.ts index cfa1fea5..93b5801a 100644 --- a/packages/ttsl-lang/src/language/validation/safe-ds-validator.ts +++ b/packages/ttsl-lang/src/language/validation/safe-ds-validator.ts @@ -1,5 +1,5 @@ import { ValidationChecks } from 'langium'; -import { SafeDsAstType } from '../generated/ast.js'; +import { TTSLAstType } from '../generated/ast.js'; import type { SafeDsServices } from '../safe-ds-module.js'; import { annotationCallAnnotationShouldNotBeDeprecated, @@ -191,7 +191,7 @@ import { */ export const registerValidationChecks = function (services: SafeDsServices) { const registry = services.validation.ValidationRegistry; - const checks: ValidationChecks = { + const checks: ValidationChecks = { TslAssignee: [ assigneeAssignedResultShouldNotBeDeprecated(services), assigneeAssignedResultShouldNotBeExperimental(services), diff --git a/packages/ttsl-lang/src/language/workspace/safe-ds-package-manager.ts b/packages/ttsl-lang/src/language/workspace/safe-ds-package-manager.ts index af30ce2e..945e60b2 100644 --- a/packages/ttsl-lang/src/language/workspace/safe-ds-package-manager.ts +++ b/packages/ttsl-lang/src/language/workspace/safe-ds-package-manager.ts @@ -9,7 +9,7 @@ import { LangiumDocuments, } from 'langium'; import { isTslSegment } from '../generated/ast.js'; -import { getPackageName, isInternal } from '../helpers/nodeProperties.js'; +import { getPackageName, isPackagePrivate } from '../helpers/nodeProperties.js'; export class SafeDsPackageManager { private readonly astNodeLocator: AstNodeLocator; @@ -112,7 +112,7 @@ export class SafeDsPackageManager { } if (hideInternal) { - result = result.filter((it) => !isTslSegment(it.node) || !isInternal(it.node)); + result = result.filter((it) => !isTslSegment(it.node) || !isPackagePrivate(it.node)); } return result; diff --git a/packages/ttsl-lang/src/language/workspace/safe-ds-settings-provider.ts b/packages/ttsl-lang/src/language/workspace/safe-ds-settings-provider.ts index 737b15d4..07651450 100644 --- a/packages/ttsl-lang/src/language/workspace/safe-ds-settings-provider.ts +++ b/packages/ttsl-lang/src/language/workspace/safe-ds-settings-provider.ts @@ -1,6 +1,6 @@ import { ConfigurationProvider } from 'langium'; import { SafeDsServices } from '../safe-ds-module.js'; -import { SafeDsLanguageMetaData } from '../generated/module.js'; +import { TTSLLanguageMetaData } from '../generated/module.js'; export class SafeDsSettingsProvider { private readonly configurationProvider: ConfigurationProvider; @@ -26,9 +26,7 @@ export class SafeDsSettingsProvider { } private async getValidationSettings(): Promise> { - return ( - (await this.configurationProvider.getConfiguration(SafeDsLanguageMetaData.languageId, 'validation')) ?? {} - ); + return (await this.configurationProvider.getConfiguration(TTSLLanguageMetaData.languageId, 'validation')) ?? {}; } } diff --git a/packages/ttsl-lang/tests/language/runner/messages.test.ts b/packages/ttsl-lang/tests/language/runner/messages.test.ts deleted file mode 100644 index ab8d17e2..00000000 --- a/packages/ttsl-lang/tests/language/runner/messages.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { ToStringTest } from '../../helpers/testDescription.js'; -import { - createPlaceholderQueryMessage, - createProgramMessage, - createShutdownMessage, - PythonServerMessage, -} from '../../../src/language/runner/messages.js'; - -describe('runner messages', async () => { - const toStringTests: ToStringTest<() => PythonServerMessage>[] = [ - { - value: () => - createProgramMessage('abcdefgh', { - code: { - a: { - gen_test_a: 'def pipe():\n\tpass\n', - gen_test_a_pipe: "from gen_test_a import pipe\n\nif __name__ == '__main__':\n\tpipe()", - }, - }, - main: { modulepath: 'a', module: 'test_a', pipeline: 'pipe' }, - }), - expectedString: - '{"type":"program","id":"abcdefgh","data":{"code":{"a":{"gen_test_a":"def pipe():\\n\\tpass\\n","gen_test_a_pipe":"from gen_test_a import pipe\\n\\nif __name__ == \'__main__\':\\n\\tpipe()"}},"main":{"modulepath":"a","module":"test_a","pipeline":"pipe"}}}', - }, - { - value: () => createPlaceholderQueryMessage('abcdefg', 'value1', 2, 1), - expectedString: - '{"type":"placeholder_query","id":"abcdefg","data":{"name":"value1","window":{"begin":2,"size":1}}}', - }, - { - value: () => createPlaceholderQueryMessage('abcdefg', 'value1', 1), - expectedString: '{"type":"placeholder_query","id":"abcdefg","data":{"name":"value1","window":{"begin":1}}}', - }, - { - value: () => createPlaceholderQueryMessage('abcdefg', 'value1', undefined, 1), - expectedString: '{"type":"placeholder_query","id":"abcdefg","data":{"name":"value1","window":{"size":1}}}', - }, - { - value: () => createPlaceholderQueryMessage('abcdefg', 'value1'), - expectedString: '{"type":"placeholder_query","id":"abcdefg","data":{"name":"value1","window":{}}}', - }, - { - value: () => createShutdownMessage(), - expectedString: '{"type":"shutdown","id":"","data":""}', - }, - ]; - - describe.each(toStringTests)('stringify', ({ value, expectedString }) => { - it(`should return the expected JSON representation of runner message (type: ${JSON.parse(expectedString).type})`, () => { - expect(JSON.stringify(value())).toStrictEqual(expectedString); - }); - }); -}); diff --git a/packages/ttsl-lang/tests/language/runner/safe-ds-runner.test.ts b/packages/ttsl-lang/tests/language/runner/safe-ds-runner.test.ts deleted file mode 100644 index 85bff951..00000000 --- a/packages/ttsl-lang/tests/language/runner/safe-ds-runner.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { NodeFileSystem } from 'langium/node'; -import net from 'net'; -import { URI } from 'langium'; -import { createSafeDsServices } from '../../../src/language/index.js'; - -const services = (await createSafeDsServices(NodeFileSystem)).SafeDs; -const runner = services.runtime.Runner; - -describe('SafeDsRunner', async () => { - describe('findFirstFreePort', async () => { - it('occupied', async () => { - const server = net.createServer(); - server.on('error', (err: any) => { - throw err; - }); - const portNumber = 46821; - await new Promise((resolve, _reject) => { - server.listen(portNumber, '127.0.0.1', () => { - resolve(); - }); - }); - const foundPort = await runner.findFirstFreePort(portNumber); - server.close(); - expect(foundPort).toStrictEqual(portNumber + 1); - }); - it('available', async () => { - const portNumber = 46825; - const foundPort = await runner.findFirstFreePort(portNumber); - expect(foundPort).toStrictEqual(portNumber); - }); - }); - describe('getMainModuleName', async () => { - it('Tslpipe', async () => { - const document = services.shared.workspace.LangiumDocumentFactory.fromString( - '', - URI.file('/a-b c.Tslpipe'), - ); - const mainModuleName = runner.getMainModuleName(document); - expect(mainModuleName).toBe('a_b_c'); - }); - it('Tsltest', async () => { - const document = services.shared.workspace.LangiumDocumentFactory.fromString( - '', - URI.file('/a-b c.Tsltest'), - ); - const mainModuleName = runner.getMainModuleName(document); - expect(mainModuleName).toBe('a_b_c'); - }); - it('other', async () => { - const document = services.shared.workspace.LangiumDocumentFactory.fromString( - '', - URI.file('/a-b c.Tsltest2'), - ); - const mainModuleName = runner.getMainModuleName(document); - expect(mainModuleName).toBe('a_b_c_Tsltest2'); - }); - }); - describe('generateCodeForRunner', async () => { - it('generateCodeForRunner', async () => { - const document = services.shared.workspace.LangiumDocumentFactory.fromString( - 'package a\n\npipeline mainpipeline {}', - URI.file('/b.Tsltest'), - ); - const [programCodeMap] = runner.generateCodeForRunner(document, undefined); - expect(JSON.stringify(programCodeMap).replaceAll('\\r\\n', '\\n')).toBe( - '{"a":{"gen_b":"# Pipelines --------------------------------------------------------------------\\n\\ndef mainpipeline():\\n pass\\n","gen_b_mainpipeline":"from .gen_b import mainpipeline\\n\\nif __name__ == \'__main__\':\\n mainpipeline()\\n"}}', - ); - }); - }); -}); diff --git a/packages/ttsl-vscode/package.json b/packages/ttsl-vscode/package.json index beab2910..cc83d095 100644 --- a/packages/ttsl-vscode/package.json +++ b/packages/ttsl-vscode/package.json @@ -34,7 +34,7 @@ "color": "#e9eded" }, "icon": "img/safe-ds_logo_rounded_128x128.png", - "qna": "https://github.com/orgs/Safe-DS/discussions", + "qna": "https://github.com/SEEDS-Group/TTSL/discussions", "badges": [ { "url": "https://github.com/Safe-DS/DSL/actions/workflows/main.yml/badge.svg", @@ -72,67 +72,47 @@ } ], "configuration": { - "title": "Safe-DS", + "title": "TTSL", "properties": { - "safe-ds.runner.command": { - "description": "Command to start the Safe-DS runner", - "type": "string", - "default": "safe-ds-runner" - }, - "safe-ds.validation.codeStyle": { + "ttsl.validation.codeStyle": { "type": "boolean", "default": true, "description": "Show an info if code style can be improved." }, - "safe-ds.validation.experimentalLanguageFeature": { + "ttsl.validation.experimentalLanguageFeature": { "type": "boolean", "default": true, "description": "Warn if an experimental language feature is used." }, - "safe-ds.validation.experimentalLibraryElement": { + "ttsl.validation.experimentalLibraryElement": { "type": "boolean", "default": true, "description": "Warn if an experimental library element is used." }, - "safe-ds.validation.nameConvention": { + "ttsl.validation.nameConvention": { "type": "boolean", "default": true, - "description": "Warn if a name does not match the Safe-DS name convention." + "description": "Warn if a name does not match the TTSL name convention." } } }, "configurationDefaults": { - "[safe-ds]": { + "[ttsl]": { "editor.semanticHighlighting.enabled": true, "editor.wordSeparators": "`~!@#%^&*()-=+[]{}\\|;:'\",.<>/?»«", "files.trimTrailingWhitespace": true } }, - "menus": { - "editor/title/run": [ - { - "command": "safe-ds.runPipelineFile", - "when": "resourceLangId == safe-ds", - "group": "navigation@1" - } - ] - }, "commands": [ { - "command": "safe-ds.dumpDiagnostics", + "command": "ttsl.dumpDiagnostics", "title": "Dump Diagnostics to JSON", "category": "TTSL" }, { - "command": "safe-ds.openDiagnosticsDumps", + "command": "ttsl.openDiagnosticsDumps", "title": "Open Diagnostics Dumps in New VS Code Window", "category": "TTSL" - }, - { - "command": "safe-ds.runPipelineFile", - "title": "Run Pipeline", - "category": "TTSL", - "icon": "$(play)" } ], "snippets": [ diff --git a/packages/ttsl-vscode/src/extension/mainClient.ts b/packages/ttsl-vscode/src/extension/mainClient.ts index 178705f4..a546933e 100644 --- a/packages/ttsl-vscode/src/extension/mainClient.ts +++ b/packages/ttsl-vscode/src/extension/mainClient.ts @@ -2,41 +2,21 @@ import * as path from 'node:path'; import * as vscode from 'vscode'; import type { LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node.js'; import { LanguageClient, TransportKind } from 'vscode-languageclient/node.js'; -import { ast, createSafeDsServices, getModuleMembers, messages, SafeDsServices } from '@ttsl/lang'; -import { NodeFileSystem } from 'langium/node'; -import { getSafeDSOutputChannel, initializeLog, logError, logOutput, printOutputMessage } from './output.js'; -import crypto from 'crypto'; -import { LangiumDocument, URI } from 'langium'; +import { getSafeDSOutputChannel, initializeLog } from './output.js'; import { dumpDiagnostics } from './commands/dumpDiagnostics.js'; import { openDiagnosticsDumps } from './commands/openDiagnosticsDumps.js'; let client: LanguageClient; -let services: SafeDsServices; // This function is called when the extension is activated. export const activate = async function (context: vscode.ExtensionContext) { initializeLog(); client = startLanguageClient(context); - const runnerCommandSetting = vscode.workspace.getConfiguration('safe-ds.runner').get('command')!; // Default is set - services = (await createSafeDsServices(NodeFileSystem, { runnerCommand: runnerCommandSetting })).SafeDs; - services.runtime.Runner.updateRunnerLogging({ - displayError(value: string): void { - vscode.window.showErrorMessage(value); - }, - outputError(value: string): void { - logError(value); - }, - outputInfo(value: string): void { - logOutput(value); - }, - }); - await services.runtime.Runner.startPythonServer(); acceptRunRequests(context); }; // This function is called when the extension is deactivated. export const deactivate = async function (): Promise { - await services.runtime.Runner.stopPythonServer(); if (client) { await client.stop(); } @@ -62,7 +42,7 @@ const startLanguageClient = function (context: vscode.ExtensionContext): Languag debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions }, }; - const fileSystemWatcher = vscode.workspace.createFileSystemWatcher('**/*.{Tslpipe,Tslstub,Tsltest}'); + const fileSystemWatcher = vscode.workspace.createFileSystemWatcher('**/*.ttsl'); context.subscriptions.push(fileSystemWatcher); // Options to control the language client @@ -84,191 +64,13 @@ const startLanguageClient = function (context: vscode.ExtensionContext): Languag }; const acceptRunRequests = function (context: vscode.ExtensionContext) { - // Register logging message callbacks - registerMessageLoggingCallbacks(); // Register VS Code Entry Points registerVSCodeCommands(context); - // Register watchers - registerVSCodeWatchers(); -}; - -const registerMessageLoggingCallbacks = function () { - services.runtime.Runner.addMessageCallback((message) => { - printOutputMessage( - `Placeholder value is (${message.id}): ${message.data.name} of type ${message.data.type} = ${message.data.value}`, - ); - }, 'placeholder_value'); - services.runtime.Runner.addMessageCallback((message) => { - printOutputMessage( - `Placeholder was calculated (${message.id}): ${message.data.name} of type ${message.data.type}`, - ); - const execInfo = services.runtime.Runner.getExecutionContext(message.id)!; - execInfo.calculatedPlaceholders.set(message.data.name, message.data.type); - // services.runtime.Runner.sendMessageToPythonServer( - // messages.createPlaceholderQueryMessage(message.id, message.data.name), - //); - }, 'placeholder_type'); - services.runtime.Runner.addMessageCallback((message) => { - printOutputMessage(`Runner-Progress (${message.id}): ${message.data}`); - }, 'runtime_progress'); - services.runtime.Runner.addMessageCallback(async (message) => { - let readableStacktraceSafeDs: string[] = []; - const execInfo = services.runtime.Runner.getExecutionContext(message.id)!; - const readableStacktracePython = await Promise.all( - (message).data.backtrace.map(async (frame) => { - const mappedFrame = await services.runtime.Runner.tryMapToSafeDSSource(message.id, frame); - if (mappedFrame) { - readableStacktraceSafeDs.push( - `\tat ${URI.file(execInfo.path)}#${mappedFrame.line} (${execInfo.path} line ${ - mappedFrame.line - })`, - ); - return `\tat ${frame.file} line ${frame.line} (mapped to '${mappedFrame.file}' line ${mappedFrame.line})`; - } - return `\tat ${frame.file} line ${frame.line}`; - }), - ); - logOutput( - `Runner-RuntimeError (${message.id}): ${ - (message).data.message - } \n${readableStacktracePython.join('\n')}`, - ); - printOutputMessage( - `Safe-DS Error (${message.id}): ${(message).data.message} \n${readableStacktraceSafeDs - .reverse() - .join('\n')}`, - ); - }, 'runtime_error'); }; const registerVSCodeCommands = function (context: vscode.ExtensionContext) { - context.subscriptions.push(vscode.commands.registerCommand('safe-ds.dumpDiagnostics', dumpDiagnostics(context))); + context.subscriptions.push(vscode.commands.registerCommand('ttsl.dumpDiagnostics', dumpDiagnostics(context))); context.subscriptions.push( - vscode.commands.registerCommand('safe-ds.openDiagnosticsDumps', openDiagnosticsDumps(context)), - ); - - context.subscriptions.push(vscode.commands.registerCommand('safe-ds.runPipelineFile', commandRunPipelineFile)); -}; - -const runPipelineFile = async function (filePath: vscode.Uri | undefined, pipelineId: string) { - let pipelinePath = filePath; - // Allow execution via command menu - if (!pipelinePath && vscode.window.activeTextEditor) { - pipelinePath = vscode.window.activeTextEditor.document.uri; - } - if ( - pipelinePath && - !services.LanguageMetaData.fileExtensions.some((extension: string) => pipelinePath!.fsPath.endsWith(extension)) - ) { - vscode.window.showErrorMessage(`Could not run ${pipelinePath!.fsPath} as it is not a Safe-DS file`); - return; - } - if (!pipelinePath) { - vscode.window.showErrorMessage('Could not run Safe-DS Pipeline, as no pipeline is currently selected.'); - return; - } - // Refresh workspace - // Do not delete builtins - services.shared.workspace.LangiumDocuments.all - .filter( - (document) => - !( - ast.isTslModule(document.parseResult.value) && - (document.parseResult.value).name === 'safeds.lang' - ), - ) - .forEach((oldDocument) => { - services.shared.workspace.LangiumDocuments.deleteDocument(oldDocument.uri); - }); - const workspaceTslFiles = await vscode.workspace.findFiles('**/*.{Tslpipe,Tslstub,Tsltest}'); - // Load all documents - const unvalidatedTslDocuments = await Promise.all( - workspaceTslFiles.map((newDocumentUri) => - services.shared.workspace.LangiumDocuments.getOrCreateDocument(newDocumentUri), - ), + vscode.commands.registerCommand('ttsl.openDiagnosticsDumps', openDiagnosticsDumps(context)), ); - // Validate them - const validationErrorMessage = await validateDocuments(services, unvalidatedTslDocuments); - if (validationErrorMessage) { - vscode.window.showErrorMessage(validationErrorMessage); - return; - } - // Run it - let mainDocument; - if (!services.shared.workspace.LangiumDocuments.hasDocument(pipelinePath)) { - mainDocument = await services.shared.workspace.LangiumDocuments.getOrCreateDocument(pipelinePath); - const mainDocumentValidationErrorMessage = await validateDocuments(services, [mainDocument]); - if (mainDocumentValidationErrorMessage) { - vscode.window.showErrorMessage(mainDocumentValidationErrorMessage); - return; - } - } else { - mainDocument = await services.shared.workspace.LangiumDocuments.getOrCreateDocument(pipelinePath); - } - - const firstPipeline = getModuleMembers(mainDocument.parseResult.value).find(ast.isTslPipeline); - if (firstPipeline === undefined) { - logError('Cannot execute: no pipeline found'); - vscode.window.showErrorMessage('The current file cannot be executed, as no pipeline could be found.'); - return; - } - const mainPipelineName = services.builtins.Annotations.getPythonName(firstPipeline) || firstPipeline.name; - - printOutputMessage(`Launching Pipeline (${pipelineId}): ${pipelinePath} - ${mainPipelineName}`); - - await services.runtime.Runner.executePipeline(pipelineId, mainDocument, mainPipelineName); -}; - -const commandRunPipelineFile = async function (filePath: vscode.Uri | undefined) { - await vscode.workspace.saveAll(); - await runPipelineFile(filePath, crypto.randomUUID()); -}; - -const validateDocuments = async function ( - TslServices: SafeDsServices, - documents: LangiumDocument[], -): Promise { - await TslServices.shared.workspace.DocumentBuilder.build(documents, { validation: true }); - - const errors = documents.flatMap((validatedDocument) => { - const validationInfo = { - validatedDocument, - diagnostics: (validatedDocument.diagnostics ?? []).filter((e) => e.severity === 1), - }; - return validationInfo.diagnostics.length > 0 ? [validationInfo] : []; - }); - - if (errors.length > 0) { - for (const validationInfo of errors) { - logError(`File ${validationInfo.validatedDocument.uri.toString()} has errors:`); - for (const validationError of validationInfo.diagnostics) { - logError( - `\tat line ${validationError.range.start.line + 1}: ${ - validationError.message - } [${validationInfo.validatedDocument.textDocument.getText(validationError.range)}]`, - ); - } - } - return 'Cannot run the main pipeline, because some files have errors.'; - } - return undefined; -}; - -const registerVSCodeWatchers = function () { - vscode.workspace.onDidChangeConfiguration((event) => { - if (event.affectsConfiguration('safe-ds.runner.command')) { - // Try starting runner - logOutput('Safe-DS Runner Command was updated'); - services.runtime.Runner.updateRunnerCommand( - vscode.workspace.getConfiguration('safe-ds.runner').get('command')!, - ); - if (!services.runtime.Runner.isPythonServerAvailable()) { - services.runtime.Runner.startPythonServer(); - } else { - logOutput( - 'As the Safe-DS Runner is currently successfully running, no attempt to start it will be made', - ); - } - } - }); };