Skip to content

Commit

Permalink
Extend protocol with client id and improve update mechanism
Browse files Browse the repository at this point in the history
- Use open/close as lifecycle methods for clients
- Let extension startup early to handle non-textual editors
  • Loading branch information
martin-fleck-at committed Oct 12, 2023
1 parent 639466e commit b8804c6
Show file tree
Hide file tree
Showing 20 changed files with 658 additions and 451 deletions.
2 changes: 1 addition & 1 deletion extensions/crossmodel-lang/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
]
},
"activationEvents": [
"onLanguage:cross-model"
"onStartupFinished"
],
"dependencies": {
"@crossbreeze/protocol": "0.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export class CrossModelCreateEdgeOperationHandler extends OperationHandler imple
relationshipRoot.relationship = relationship;
const text = this.state.semanticSerializer.serialize(relationshipRoot);

await this.state.modelService.save(uri.toString(), text);
await this.state.modelService.save({ uri: uri.toString(), model: text, clientId: this.state.clientId });
const root = await this.state.modelService.request<CrossModelRoot>(uri.toString(), isCrossModelRoot);
return root?.relationship;
}
Expand Down
2 changes: 1 addition & 1 deletion extensions/crossmodel-lang/src/glsp-server/launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { CrossModelLayoutConfigurator } from './layout/cross-model-layout-config
* @returns a promise that is resolved as soon as the server is shut down or rejects if an error occurs
*/
export function startGLSPServer(services: CrossModelLSPServices, workspaceFolder: URI): MaybePromise<void> {
const launchOptions: SocketLaunchOptions = { ...defaultSocketLaunchOptions, logLevel: LogLevel.debug };
const launchOptions: SocketLaunchOptions = { ...defaultSocketLaunchOptions, logLevel: LogLevel.info };

// create module based on launch options, e.g., logging etc.
const appModule = createAppModule(launchOptions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ export class CrossModelState extends DefaultModelState {
}

async updateSemanticRoot(content?: string): Promise<void> {
this._semanticRoot = await this.modelService.update(this.semanticUri, content ?? this.semanticRoot);
this._semanticRoot = await this.modelService.update({
uri: this.semanticUri,
model: content ?? this.semanticRoot,
clientId: this.clientId
});
this.index.indexSemanticRoot(this.semanticRoot);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@
********************************************************************************/

import {
ActionDispatcher,
ClientSession,
ClientSessionListener,
ClientSessionManager,
GLSPServerError,
Logger,
MaybePromise,
ModelSubmissionHandler,
RequestModelAction,
SOURCE_URI_ARG,
SaveModelAction,
SourceModelStorage
ActionDispatcher,
ClientSession,
ClientSessionListener,
ClientSessionManager,
Disposable,
DisposableCollection,
GLSPServerError,
Logger,
MaybePromise,
ModelSubmissionHandler,
RequestModelAction,
SOURCE_URI_ARG,
SaveModelAction,
SourceModelStorage
} from '@eclipse-glsp/server';
import { inject, injectable, postConstruct } from 'inversify';
import { findRootNode, streamReferences } from 'langium';
Expand All @@ -29,62 +31,71 @@ import { CrossModelState } from './cross-model-state';
*/
@injectable()
export class CrossModelStorage implements SourceModelStorage, ClientSessionListener {
@inject(Logger) protected logger: Logger;
@inject(CrossModelState) protected state: CrossModelState;
@inject(ClientSessionManager) protected sessionManager: ClientSessionManager;
@inject(ModelSubmissionHandler) protected submissionHandler: ModelSubmissionHandler;
@inject(ActionDispatcher) protected actionDispatcher: ActionDispatcher;
@inject(Logger) protected logger: Logger;
@inject(CrossModelState) protected state: CrossModelState;
@inject(ClientSessionManager) protected sessionManager: ClientSessionManager;
@inject(ModelSubmissionHandler) protected submissionHandler: ModelSubmissionHandler;
@inject(ActionDispatcher) protected actionDispatcher: ActionDispatcher;

@postConstruct()
protected init(): void {
this.sessionManager.addListener(this, this.state.clientId);
}
protected toDispose = new DisposableCollection();

async loadSourceModel(action: RequestModelAction): Promise<void> {
// load semantic model from document in language model service
const sourceUri = this.getSourceUri(action);
const rootUri = URI.file(sourceUri).toString();
const root = await this.state.modelService.request(rootUri, isCrossModelRoot);
if (!root || !root.diagram) {
throw new GLSPServerError('Expected CrossModal Diagram Root');
}
this.state.setSemanticRoot(rootUri, root);
this.state.modelService.onUpdate<CrossModelRoot>(rootUri, async newRoot => {
this.state.setSemanticRoot(rootUri, newRoot);
this.actionDispatcher.dispatchAll(await this.submissionHandler.submitModel('external'));
});
}
@postConstruct()
protected init(): void {
this.sessionManager.addListener(this, this.state.clientId);
}

saveSourceModel(action: SaveModelAction): MaybePromise<void> {
const saveUri = this.getFileUri(action);
async loadSourceModel(action: RequestModelAction): Promise<void> {
// load semantic model from document in language model service
const sourceUri = this.getSourceUri(action);
const rootUri = URI.file(sourceUri).toString();
await this.state.modelService.open({ uri: rootUri, clientId: this.state.clientId });
this.toDispose.push(Disposable.create(() => this.state.modelService.close({ uri: rootUri, clientId: this.state.clientId })));
const root = await this.state.modelService.request(rootUri, isCrossModelRoot);
if (!root || !root.diagram) {
throw new GLSPServerError('Expected CrossModal Diagram Root');
}
this.state.setSemanticRoot(rootUri, root);
this.toDispose.push(
this.state.modelService.onUpdate<CrossModelRoot>(rootUri, async event => {
if (this.state.clientId !== event.sourceClientId) {
this.state.setSemanticRoot(rootUri, event.model);
this.actionDispatcher.dispatchAll(await this.submissionHandler.submitModel('external'));
}
})
);
}

// save document and all related documents
this.state.modelService.save(saveUri, this.state.semanticRoot);
streamReferences(this.state.semanticRoot)
.map(refInfo => refInfo.reference.ref)
.nonNullable()
.map(ref => findRootNode(ref))
.forEach(root => this.state.modelService.save(root.$document!.uri.toString(), root));
}
saveSourceModel(action: SaveModelAction): MaybePromise<void> {
const saveUri = this.getFileUri(action);

sessionDisposed(_clientSession: ClientSession): void {
// close loaded document for modification
this.state.modelService.close(this.state.semanticUri);
}
// save document and all related documents
this.state.modelService.save({ uri: saveUri, model: this.state.semanticRoot, clientId: this.state.clientId });
streamReferences(this.state.semanticRoot)
.map(refInfo => refInfo.reference.ref)
.nonNullable()
.map(ref => findRootNode(ref))
.forEach(root =>
this.state.modelService.save({ uri: root.$document!.uri.toString(), model: root, clientId: this.state.clientId })
);
}

protected getSourceUri(action: RequestModelAction): string {
const sourceUri = action.options?.[SOURCE_URI_ARG];
if (typeof sourceUri !== 'string') {
throw new GLSPServerError(`Invalid RequestModelAction! Missing argument with key '${SOURCE_URI_ARG}'`);
}
return sourceUri;
}
sessionDisposed(_clientSession: ClientSession): void {
this.toDispose.dispose();
}

protected getFileUri(action: SaveModelAction): string {
const uri = action.fileUri ?? this.state.get(SOURCE_URI_ARG);
if (!uri) {
throw new GLSPServerError('Could not derive fileUri for saving the current source model');
}
return uri;
}
protected getSourceUri(action: RequestModelAction): string {
const sourceUri = action.options?.[SOURCE_URI_ARG];
if (typeof sourceUri !== 'string') {
throw new GLSPServerError(`Invalid RequestModelAction! Missing argument with key '${SOURCE_URI_ARG}'`);
}
return sourceUri;
}

protected getFileUri(action: SaveModelAction): string {
const uri = action.fileUri ?? this.state.get(SOURCE_URI_ARG);
if (!uri) {
throw new GLSPServerError('Could not derive fileUri for saving the current source model');
}
return uri;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export const CrossModelSharedModule: Module<
WorkspaceManager: services => new CrossModelWorkspaceManager(services),
PackageManager: services => new CrossModelPackageManager(services),
LangiumDocuments: services => new CrossModelLangiumDocuments(services),
TextDocuments: () => new OpenableTextDocuments(TextDocument),
TextDocuments: services => new OpenableTextDocuments(TextDocument, services.logger.ClientLogger),
TextDocumentManager: services => new OpenTextDocumentManager(services),
DocumentBuilder: services => new CrossModelDocumentBuilder(services)
},
Expand Down
74 changes: 51 additions & 23 deletions extensions/crossmodel-lang/src/model-server/model-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@

import {
CloseModel,
CloseModelArgs,
CrossModelRoot,
OnSave,
OnUpdated,
OpenModel,
OpenModelArgs,
RequestModel,
RequestModelDiagramNode,
SaveModel,
UpdateModel
SaveModelArgs,
UpdateModel,
UpdateModelArgs
} from '@crossbreeze/protocol';
import { AstNode, isReference } from 'langium';
import { isReference } from 'langium';
import { Disposable } from 'vscode-jsonrpc';
import * as rpc from 'vscode-jsonrpc/node';
import { CrossModelRoot as CrossModelRootAst, DiagramNode, Entity, isCrossModelRoot } from '../language-server/generated/ast';
Expand All @@ -26,18 +30,19 @@ import { ModelService } from './model-service';
*/
export class ModelServer implements Disposable {
protected toDispose: Disposable[] = [];
protected toDisposeForSession: Map<string, Disposable[]> = new Map();

constructor(protected connection: rpc.MessageConnection, protected modelService: ModelService) {
this.initialize(connection);
}

protected initialize(connection: rpc.MessageConnection): void {
this.toDispose.push(connection.onRequest(OpenModel, uri => this.openModel(uri)));
this.toDispose.push(connection.onRequest(CloseModel, uri => this.closeModel(uri)));
this.toDispose.push(connection.onRequest(OpenModel, args => this.openModel(args)));
this.toDispose.push(connection.onRequest(CloseModel, args => this.closeModel(args)));
this.toDispose.push(connection.onRequest(RequestModel, uri => this.requestModel(uri)));
this.toDispose.push(connection.onRequest(RequestModelDiagramNode, (uri, id) => this.requestModelDiagramNode(uri, id)));
this.toDispose.push(connection.onRequest(UpdateModel, (uri, model) => this.updateModel(uri, model)));
this.toDispose.push(connection.onRequest(SaveModel, (uri, model) => this.saveModel(uri, model)));
this.toDispose.push(connection.onRequest(UpdateModel, args => this.updateModel(args)));
this.toDispose.push(connection.onRequest(SaveModel, args => this.saveModel(args)));
}

/**
Expand Down Expand Up @@ -81,35 +86,58 @@ export class ModelServer implements Disposable {
};
}

protected async openModel(uri: string): Promise<void> {
await this.modelService.open(uri);

this.modelService.onSave(uri, newModel => {
// TODO: Research if this also has to be closed after the document closes
this.connection.sendNotification(OnSave, uri, toSerializable(newModel) as CrossModelRoot);
});
this.modelService.onUpdate(uri, newModel => {
// TODO: Research if this also has to be closed after the document closes
this.connection.sendNotification(OnUpdated, uri, toSerializable(newModel) as CrossModelRoot);
});
protected async openModel(args: OpenModelArgs): Promise<CrossModelRoot | undefined> {
if (!this.modelService.isOpen(args.uri)) {
await this.modelService.open(args);
}
this.setupListeners(args);
return this.requestModel(args.uri);
}

protected setupListeners(args: OpenModelArgs): void {
this.disposeListeners(args);
const listenersForClient = [];
listenersForClient.push(
this.modelService.onSave(args.uri, event =>
this.connection.sendNotification(OnSave, {
uri: args.uri,
model: toSerializable(event.model) as CrossModelRoot,
sourceClientId: event.sourceClientId
})
),
this.modelService.onUpdate(args.uri, event =>
this.connection.sendNotification(OnUpdated, {
uri: args.uri,
model: toSerializable(event.model) as CrossModelRoot,
sourceClientId: event.sourceClientId
})
)
);
this.toDisposeForSession.set(args.clientId, listenersForClient);
}

protected disposeListeners(args: CloseModelArgs): void {
this.toDisposeForSession.get(args.clientId)?.forEach(disposable => disposable.dispose());
this.toDisposeForSession.delete(args.clientId);
}

protected async closeModel(uri: string): Promise<void> {
await this.modelService.close(uri);
protected async closeModel(args: CloseModelArgs): Promise<void> {
this.disposeListeners(args);
return this.modelService.close(args);
}

protected async requestModel(uri: string): Promise<CrossModelRoot | undefined> {
const root = await this.modelService.request(uri, isCrossModelRoot);
return toSerializable(root) as CrossModelRoot;
}

protected async updateModel(uri: string, model: CrossModelRoot): Promise<CrossModelRoot> {
const updated = await this.modelService.update(uri, model);
protected async updateModel(args: UpdateModelArgs<CrossModelRoot>): Promise<CrossModelRoot> {
const updated = await this.modelService.update(args);
return toSerializable(updated) as CrossModelRoot;
}

protected async saveModel(uri: string, model: AstNode): Promise<void> {
await this.modelService.save(uri, model);
protected async saveModel(args: SaveModelArgs<CrossModelRoot>): Promise<void> {
await this.modelService.save(args);
}

dispose(): void {
Expand Down
Loading

0 comments on commit b8804c6

Please sign in to comment.