Skip to content

Commit

Permalink
feat: add more services to statemachine example
Browse files Browse the repository at this point in the history
- add `Formatting` service
- add `CodeActionProvider` service
    - Remove an unreachable commands, events, and states
    - Rename all refereed state names that don't start with caps
- validate unreached commands, events, and states
  • Loading branch information
Yokozuna59 committed Apr 29, 2024
1 parent 3a5a539 commit e3757c3
Show file tree
Hide file tree
Showing 5 changed files with 333 additions and 9 deletions.
2 changes: 1 addition & 1 deletion examples/statemachine/example/trafficlight.statemachine
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
statemachine TrafficLight

events
events
switchCapacity
next

Expand Down
151 changes: 151 additions & 0 deletions examples/statemachine/src/language-server/statemachine-code-actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/******************************************************************************
* Copyright 2021 TypeFox GmbH
* This program and the accompanying materials are made available under the
* terms of the MIT License, which is available in the project root.
******************************************************************************/

import type {
AstNode,
DiagnosticData,
LangiumDocument,
MaybePromise,
Reference,
} from 'langium';
import type { CodeActionProvider } from 'langium/lsp';
import type {
CodeActionParams,
Command,
CodeAction,
Diagnostic,
TextEdit,
Range,
Position,
} from 'vscode-languageserver';
import { CodeActionKind } from 'vscode-languageserver';
import { IssueCodes } from './statemachine-validator.js';
import type { State, Statemachine } from './generated/ast.js';

export class StatemachineCodeActionProvider implements CodeActionProvider {
getCodeActions(
document: LangiumDocument<AstNode>,
params: CodeActionParams
): MaybePromise<Array<Command | CodeAction> | undefined> {
const result: CodeAction[] = [];
const acceptor = (ca: CodeAction | undefined) => ca && result.push(ca);
for (const diagnostic of params.context.diagnostics) {
this.createCodeActions(diagnostic, document, acceptor);
}
return result;
}

private createCodeActions(
diagnostic: Diagnostic,
document: LangiumDocument,
accept: (ca: CodeAction | undefined) => void
): void {
switch ((diagnostic.data as DiagnosticData)?.code) {
case IssueCodes.StateNameUppercase:
accept(this.makeUpperCase(diagnostic, document));
break;
case IssueCodes.UnreachedState:
case IssueCodes.UnreachedCommand:
case IssueCodes.UnreachedEvent:
accept(this.removeUnusedSymbol(diagnostic, document));
break;
}
return undefined;
}

private makeUpperCase(
diagnostic: Diagnostic,
document: LangiumDocument
): CodeAction {
const changes: TextEdit[] = [];

const stateName = document.textDocument.getText(diagnostic.range);
const { init, states } = document.parseResult.value as Statemachine;
this.updateChangesForReferencedState(init, stateName, document, changes);

states.forEach(({ transitions }) => {
transitions.forEach(({ state }) => {
this.updateChangesForReferencedState(
state,
stateName,
document,
changes
);
});
});

const range = this.getFirstLetterRange(diagnostic.range.start);
changes.push(this.createTextEditForState(range, document));
return {
title: 'First letter to upper case',
kind: CodeActionKind.QuickFix,
diagnostics: [diagnostic],
isPreferred: true,
edit: {
changes: {
[document.textDocument.uri]: changes,
},
},
};
}

private createTextEditForState(
range: Range,
document: LangiumDocument
): TextEdit {
const changeRange = this.getFirstLetterRange(range.start);
return {
range: changeRange,
newText: document.textDocument.getText(changeRange).toUpperCase(),
};
}

private updateChangesForReferencedState(
state: Reference<State>,
name: string,
document: LangiumDocument,
changes: TextEdit[]
): void {
if (state.$refNode && state.ref && state.ref.name === name) {
const { range } = state.$refNode;
const changeRange = this.getFirstLetterRange(range.start);
changes.push(this.createTextEditForState(changeRange, document));
}
}

private getFirstLetterRange(position: Position): Range {
const range: Range = {
start: position,
end: {
line: position.line,
character: position.character + 1,
},
};
return range;
}

private removeUnusedSymbol(
diagnostic: Diagnostic,
document: LangiumDocument
): CodeAction {
return {
title: 'Remove unsed symbol',
kind: CodeActionKind.QuickFix,
diagnostics: [diagnostic],
isPreferred: true,
edit: {
changes: {
[document.textDocument.uri]: [
{
range: diagnostic.range,
newText: '',
},
],
},
},
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/******************************************************************************
* Copyright 2021 TypeFox GmbH
* This program and the accompanying materials are made available under the
* terms of the MIT License, which is available in the project root.
******************************************************************************/

import type { AstNode } from 'langium';
import { AbstractFormatter, Formatting } from 'langium/lsp';
import * as ast from './generated/ast.js';

export class StatemachineFormatter extends AbstractFormatter {
protected format(node: AstNode): void {
if (ast.isState(node)) {
const formatter = this.getNodeFormatter(node);
formatter.keyword('state')
.prepend(Formatting.newLine({ allowMore: true }))
.append(Formatting.oneSpace());

formatter.keyword('actions').append(Formatting.oneSpace());
const bracesOpen = formatter.keyword('{');
bracesOpen.prepend(Formatting.fit(Formatting.oneSpace(), Formatting.newLine()));
const bracesClose = formatter.keyword('}');
bracesClose.prepend(Formatting.newLine());
formatter.interior(bracesOpen, bracesClose).prepend(Formatting.indent());

const stateName = formatter.property('name');
const stateEnd = formatter.keyword('end');
formatter.interior(stateName, stateEnd).prepend(Formatting.indent());
stateEnd.prepend(Formatting.newLine());
} else if (ast.isStatemachine(node)) {
const formatter = this.getNodeFormatter(node);

formatter.keyword('statemachine').append(Formatting.oneSpace());
formatter.properties('name').append(Formatting.newLine({ allowMore: true }));

formatter.keyword('initialState')
.prepend(Formatting.newLine({ allowMore: true }))
.append(Formatting.oneSpace());
formatter.property('init').append(Formatting.newLine({ allowMore: true }));

formatter.keyword('commands')
.prepend(Formatting.newLine({ allowMore: true }));
formatter.keyword('events')
.prepend(Formatting.newLine({ allowMore: true }));
const nodes = formatter.nodes(...node.commands, ...node.events);
nodes.prepend(Formatting.indent());
} else if (ast.isTransition(node)) {
const formatter = this.getNodeFormatter(node);
formatter.keyword('=>').surround(Formatting.oneSpace());
}
}
}
10 changes: 10 additions & 0 deletions examples/statemachine/src/language-server/statemachine-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@ import type { LangiumServices, LangiumSharedServices, PartialLangiumServices } f
import { createDefaultModule, createDefaultSharedModule, type DefaultSharedModuleContext } from 'langium/lsp';
import { StatemachineGeneratedModule, StatemachineGeneratedSharedModule } from './generated/module.js';
import { StatemachineValidator, registerValidationChecks } from './statemachine-validator.js';
import { StatemachineCodeActionProvider } from './statemachine-code-actions.js';
import { StatemachineFormatter } from './statemachine-formatter.js';

/**
* Declaration of custom services - add your own service classes here.
*/
export type StatemachineAddedServices = {
lsp: {
CodeActionProvider: StatemachineCodeActionProvider;
Formatter: StatemachineFormatter;
},
validation: {
StatemachineValidator: StatemachineValidator
}
Expand All @@ -31,6 +37,10 @@ export type StatemachineServices = LangiumServices & StatemachineAddedServices
* selected services, while the custom services must be fully specified.
*/
export const StatemachineModule: Module<StatemachineServices, PartialLangiumServices & StatemachineAddedServices> = {
lsp: {
CodeActionProvider: () => new StatemachineCodeActionProvider(),
Formatter: () => new StatemachineFormatter(),
},
validation: {
StatemachineValidator: () => new StatemachineValidator()
}
Expand Down
Loading

0 comments on commit e3757c3

Please sign in to comment.