Skip to content

Commit

Permalink
feat: show image via code lens (#1071)
Browse files Browse the repository at this point in the history
Closes #984

### Summary of Changes

Show an image via a code lens.
  • Loading branch information
lars-reimann authored Apr 21, 2024
1 parent d9955c9 commit bd0946b
Show file tree
Hide file tree
Showing 18 changed files with 239 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { resourceNameToUri } from '../../helpers/resources.js';
import { URI } from 'langium';

const CORE_CLASSES_URI = resourceNameToUri('builtins/safeds/lang/coreClasses.sdsstub');
const IMAGE_URI = resourceNameToUri('builtins/safeds/data/image/containers/image.sdsstub');
const TABLE_URI = resourceNameToUri('builtins/safeds/data/tabular/containers/table.sdsstub');

export class SafeDsClasses extends SafeDsModuleMembers<SdsClass> {
Expand All @@ -23,6 +24,10 @@ export class SafeDsClasses extends SafeDsModuleMembers<SdsClass> {
return this.getClass('Int');
}

get Image(): SdsClass | undefined {
return this.getClass('Image', IMAGE_URI);
}

get List(): SdsClass | undefined {
return this.getClass('List');
}
Expand Down
2 changes: 2 additions & 0 deletions packages/safe-ds-lang/src/language/communication/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const COMMAND_RUN_PIPELINE = 'safe-ds.runPipeline';
export const COMMAND_SHOW_IMAGE = 'safe-ds.showImage';
20 changes: 20 additions & 0 deletions packages/safe-ds-lang/src/language/communication/rpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export const RPC_RUNNER_INSTALL = 'runner/install';
export const RPC_RUNNER_START = 'runner/start';
export const RPC_RUNNER_STARTED = 'runner/started';
export const RPC_RUNNER_UPDATE = 'runner/update';
export const RPC_RUNNER_SHOW_IMAGE = 'runner/showImage';

/**
* JSON representation of an image.
*/
export interface ImageJson {
/**
* The format of the image.
*/
format: 'png';

/**
* The Base64-encoded image.
*/
bytes: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ export class SafeDsMessagingProvider {
await this.messageBroker.sendNotification(method, ...args);
} else if (this.connection) {
/* c8 ignore next 2 */
await this.connection.sendNotification(method, args);
await this.connection.sendNotification(method, ...args);
}
}

Expand Down
16 changes: 3 additions & 13 deletions packages/safe-ds-lang/src/language/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
import {
RPC_RUNNER_INSTALL,
RPC_RUNNER_START,
RPC_RUNNER_STARTED,
RPC_RUNNER_UPDATE,
} from './runtime/safe-ds-runner.js';
import { pipVersionRange } from './runtime/safe-ds-python-server.js';

// Services
Expand All @@ -26,13 +20,9 @@ export { locationToString, positionToString, rangeToString } from '../helpers/lo
// Messages
export * as messages from './runtime/messages.js';

// Remote procedure calls
export const rpc = {
runnerInstall: RPC_RUNNER_INSTALL,
runnerStart: RPC_RUNNER_START,
runnerStarted: RPC_RUNNER_STARTED,
runnerUpdate: RPC_RUNNER_UPDATE,
};
// Constants
export * as commands from './communication/commands.js';
export * as rpc from './communication/rpc.js';

// Dependencies
export const dependencies = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { isSdsModule, isSdsPipeline, SdsModuleMember, SdsPipeline, SdsPlaceholde
import { SafeDsRunner } from '../runtime/safe-ds-runner.js';
import { getModuleMembers, streamPlaceholders } from '../helpers/nodeProperties.js';
import { SafeDsTypeChecker } from '../typing/safe-ds-type-checker.js';
import { COMMAND_RUN_PIPELINE } from './safe-ds-execute-command-handler.js';

import { COMMAND_RUN_PIPELINE, COMMAND_SHOW_IMAGE } from '../communication/commands.js';

export class SafeDsCodeLensProvider implements CodeLensProvider {
private readonly astNodeLocator: AstNodeLocator;
Expand Down Expand Up @@ -87,7 +88,19 @@ export class SafeDsCodeLensProvider implements CodeLensProvider {
return;
}

if (this.typeChecker.isTabular(this.typeComputer.computeType(node))) {
if (this.typeChecker.isImage(this.typeComputer.computeType(node))) {
const documentUri = AstUtils.getDocument(node).uri.toString();
const nodePath = this.astNodeLocator.getAstNodePath(node);

accept({
range: cstNode.range,
command: {
title: `Show ${node.name}`,
command: COMMAND_SHOW_IMAGE,
arguments: [documentUri, nodePath],
},
});
} else if (this.typeChecker.isTabular(this.typeComputer.computeType(node))) {
accept({
range: cstNode.range,
command: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { AbstractExecuteCommandHandler, ExecuteCommandAcceptor } from 'langium/lsp';
import { SafeDsSharedServices } from '../safe-ds-module.js';
import { SafeDsRunner } from '../runtime/safe-ds-runner.js';

export const COMMAND_RUN_PIPELINE = 'safe-ds.runPipeline';
import { COMMAND_RUN_PIPELINE, COMMAND_SHOW_IMAGE } from '../communication/commands.js';

/* c8 ignore start */
export class SafeDsExecuteCommandHandler extends AbstractExecuteCommandHandler {
Expand All @@ -17,6 +16,7 @@ export class SafeDsExecuteCommandHandler extends AbstractExecuteCommandHandler {

override registerCommands(acceptor: ExecuteCommandAcceptor) {
acceptor(COMMAND_RUN_PIPELINE, ([documentUri, nodePath]) => this.runner.runPipeline(documentUri, nodePath));
acceptor(COMMAND_SHOW_IMAGE, ([documentUri, nodePath]) => this.runner.showImage(documentUri, nodePath));
}
}
/* c8 ignore stop */
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { SafeDsServices } from '../safe-ds-module.js';
import treeKill from 'tree-kill';
import { SafeDsLogger, SafeDsMessagingProvider } from '../communication/safe-ds-messaging-provider.js';
import child_process from 'child_process';
import { RPC_RUNNER_INSTALL, RPC_RUNNER_START, RPC_RUNNER_STARTED, RPC_RUNNER_UPDATE } from './safe-ds-runner.js';
import WebSocket from 'ws';
import { createShutdownMessage, PythonServerMessage } from './messages.js';
import { Disposable } from 'langium';
import { SafeDsSettingsProvider } from '../workspace/safe-ds-settings-provider.js';
import semver from 'semver';
import net, { AddressInfo } from 'node:net';
import { ChildProcessWithoutNullStreams } from 'node:child_process';
import { RPC_RUNNER_INSTALL, RPC_RUNNER_START, RPC_RUNNER_STARTED, RPC_RUNNER_UPDATE } from '../communication/rpc.js';

const LOWEST_SUPPORTED_RUNNER_VERSION = '0.11.0';
const LOWEST_UNSUPPORTED_RUNNER_VERSION = '0.12.0';
Expand All @@ -33,13 +33,13 @@ export class SafeDsPythonServer {

// Restart if the runner command changes
services.workspace.SettingsProvider.onRunnerCommandUpdate(async () => {
await this.restart();
await this.restart(false);
});

// Start if specifically requested. This can happen if the updater installed a new version of the runner but the
// runner command did not have to be changed.
this.messaging.onNotification(RPC_RUNNER_START, async () => {
await this.restart();
await this.restart(false);
});

// Stop the Python server when the language server is shut down
Expand Down Expand Up @@ -121,9 +121,11 @@ export class SafeDsPythonServer {

/**
* Stop the Python server and start it again.
*
* @param shouldBeTracked Whether the restart should be tracked. If `false`, the restart will always be executed.
*/
private async restart(): Promise<void> {
if (!this.restartTracker.shouldRestart()) {
private async restart(shouldBeTracked: boolean): Promise<void> {
if (shouldBeTracked && !this.restartTracker.shouldRestart()) {
this.logger.error('Restarting too frequently. Aborting.');
return;
}
Expand Down Expand Up @@ -380,7 +382,7 @@ export class SafeDsPythonServer {
serverConnection.onclose = () => {
if (isStarted(this.state) && this.state.serverProcess) {
this.logger.error('Connection was unexpectedly closed');
this.restart();
this.restart(true);
}
};
};
Expand Down
115 changes: 107 additions & 8 deletions packages/safe-ds-lang/src/language/runtime/safe-ds-runner.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import { SafeDsServices } from '../safe-ds-module.js';
import { AstNodeLocator, LangiumDocument, LangiumDocuments, URI } from 'langium';
import { AstNodeLocator, AstUtils, LangiumDocument, LangiumDocuments, URI } from 'langium';
import path from 'path';
import { createProgramMessage, ProgramCodeMap, RuntimeErrorBacktraceFrame, RuntimeErrorMessage } from './messages.js';
import {
createPlaceholderQueryMessage,
createProgramMessage,
PlaceholderValueMessage,
ProgramCodeMap,
RuntimeErrorBacktraceFrame,
RuntimeErrorMessage,
} from './messages.js';
import { SourceMapConsumer } from 'source-map-js';
import { SafeDsAnnotations } from '../builtins/safe-ds-annotations.js';
import { SafeDsPythonGenerator } from '../generation/safe-ds-python-generator.js';
import { isSdsModule, isSdsPipeline } from '../generated/ast.js';
import { isSdsModule, isSdsPipeline, isSdsPlaceholder } from '../generated/ast.js';
import { SafeDsLogger, SafeDsMessagingProvider } from '../communication/safe-ds-messaging-provider.js';
import crypto from 'crypto';
import { SafeDsPythonServer } from './safe-ds-python-server.js';
import { RPC_RUNNER_SHOW_IMAGE } from '../communication/rpc.js';

// Most of the functionality cannot be tested automatically as a functioning runner setup would always be required

export const RPC_RUNNER_INSTALL = 'runner/install';
export const RPC_RUNNER_START = 'runner/start';
export const RPC_RUNNER_STARTED = 'runner/started';
export const RPC_RUNNER_UPDATE = 'runner/update';

const RUNNER_TAG = 'Runner';

/* c8 ignore start */
Expand Down Expand Up @@ -107,6 +110,102 @@ export class SafeDsRunner {
await this.executePipeline(pipelineExecutionId, document, pipeline.name);
}

async showImage(documentUri: string, nodePath: string) {
const uri = URI.parse(documentUri);
const document = this.langiumDocuments.getDocument(uri);
if (!document) {
this.messaging.showErrorMessage('Could not find document.');
return;
}

const root = document.parseResult.value;
const placeholder = this.astNodeLocator.getAstNode(root, nodePath);
if (!isSdsPlaceholder(placeholder)) {
this.messaging.showErrorMessage('Selected node is not a placeholder.');
return;
}

const pipeline = AstUtils.getContainerOfType(placeholder, isSdsPipeline);
if (!pipeline) {
this.messaging.showErrorMessage('Could not find pipeline.');
return;
}

const pipelineExecutionId = crypto.randomUUID();

const start = Date.now();

const progress = await this.messaging.showProgress('Safe-DS Runner', 'Starting...');

this.logger.info(
`[${pipelineExecutionId}] Showing image "${pipeline.name}/${placeholder.name}" in ${documentUri}.`,
);

const disposables = [
this.pythonServer.addMessageCallback('runtime_error', (message) => {
if (message.id === pipelineExecutionId) {
progress?.done();
disposables.forEach((it) => {
it.dispose();
});
this.messaging.showErrorMessage('An error occurred during pipeline execution.');
}
progress.done();
disposables.forEach((it) => {
it.dispose();
});
}),

this.pythonServer.addMessageCallback('placeholder_type', async (message) => {
if (message.id === pipelineExecutionId && message.data.name === placeholder.name) {
const data = await this.getPlaceholderValue(placeholder.name, pipelineExecutionId);
await this.messaging.sendNotification(RPC_RUNNER_SHOW_IMAGE, data);
}
}),

this.pythonServer.addMessageCallback('runtime_progress', (message) => {
if (message.id === pipelineExecutionId) {
progress.done();
const timeElapsed = Date.now() - start;
this.logger.info(
`[${pipelineExecutionId}] Finished showing image "${pipeline.name}/${placeholder.name}" in ${timeElapsed}ms.`,
);
disposables.forEach((it) => {
it.dispose();
});
}
}),
];

await this.executePipeline(pipelineExecutionId, document, pipeline.name, placeholder.name);
}

private async getPlaceholderValue(placeholder: string, pipelineExecutionId: string): Promise<any | undefined> {
return new Promise((resolve) => {
if (placeholder === '') {
resolve(undefined);
}

const placeholderValueCallback = (message: PlaceholderValueMessage) => {
if (message.id !== pipelineExecutionId || message.data.name !== placeholder) {
return;
}
this.pythonServer.removeMessageCallback('placeholder_value', placeholderValueCallback);
resolve(message.data.value);
};

this.pythonServer.addMessageCallback('placeholder_value', placeholderValueCallback);
this.logger.info('Getting placeholder from Runner ...');
this.pythonServer.sendMessageToPythonServer(
createPlaceholderQueryMessage(pipelineExecutionId, placeholder),
);

setTimeout(() => {
resolve(undefined);
}, 30000);
});
}

/**
* Map that contains information about an execution keyed by the execution id.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export class SafeDsCoreTypes {
return this.createCoreType(this.builtinClasses.Float);
}

get Image(): Type {
return this.createCoreType(this.builtinClasses.Image);
}

get Int(): Type {
return this.createCoreType(this.builtinClasses.Int);
}
Expand Down
15 changes: 15 additions & 0 deletions packages/safe-ds-lang/src/language/typing/safe-ds-type-checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,21 @@ export class SafeDsTypeChecker {
}
};

/**
* Checks whether {@link type} is some kind of image.
*/
isImage(type: Type): type is ClassType {
const imageOrNull = this.coreTypes.Image.withExplicitNullability(true);

return (
!type.equals(this.coreTypes.Nothing) &&
!type.equals(this.coreTypes.NothingOrNull) &&
this.isSubtypeOf(type, imageOrNull, {
ignoreTypeParameters: true,
})
);
}

/**
* Checks whether {@link type} is some kind of list (with any element type).
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,29 @@ describe('SafeDsCodeLensProvider', () => {
}`,
expectedCodeLensTitles: ['Run myPipeline'],
},
{
testName: 'pipeline with Image placeholder',
code: `pipeline myPipeline {
val a = Image.fromFile("test.png");
}`,
expectedCodeLensTitles: ['Run myPipeline', 'Show a'],
},
{
testName: 'block lambda with Image placeholder',
code: `pipeline myPipeline {
() {
val a = Image.fromFile("test.png");
};
}`,
expectedCodeLensTitles: ['Run myPipeline'],
},
{
testName: 'segment with Image placeholder',
code: `segment mySegment {
val a = Image.fromFile("test.png");
}`,
expectedCodeLensTitles: [],
},
{
testName: 'pipeline with Table placeholder',
code: `pipeline myPipeline {
Expand Down
Loading

0 comments on commit bd0946b

Please sign in to comment.