66
77import * as assert from 'node:assert' ;
88import type { CompletionItem , CompletionList , Diagnostic , DocumentSymbol , FoldingRange , FormattingOptions , Range , ReferenceParams , SemanticTokensParams , SemanticTokenTypes , TextDocumentIdentifier , TextDocumentPositionParams , WorkspaceSymbol } from 'vscode-languageserver-protocol' ;
9- import { DiagnosticSeverity , MarkupContent } from 'vscode-languageserver-types' ;
9+ import { CodeAction , DiagnosticSeverity , MarkupContent } from 'vscode-languageserver-types' ;
1010import { normalizeEOL } from '../generate/template-string.js' ;
1111import type { LangiumServices , LangiumSharedLSPServices } from '../lsp/lsp-services.js' ;
1212import { SemanticTokensDecoder } from '../lsp/semantic-token-provider.js' ;
@@ -22,7 +22,6 @@ import { URI } from '../utils/uri-utils.js';
2222import { DocumentValidator } from '../validation/document-validator.js' ;
2323import type { BuildOptions } from '../workspace/document-builder.js' ;
2424import { TextDocument , type LangiumDocument } from '../workspace/documents.js' ;
25-
2625export interface ParseHelperOptions extends BuildOptions {
2726 /**
2827 * Specifies the URI of the generated document. Will use a counter variable if not specified.
@@ -770,3 +769,103 @@ export function expectSemanticToken(tokensWithRanges: DecodedSemanticTokensWithR
770769 } ) ;
771770 expectedFunction ( result . length , 1 , `Expected one token with the specified options but found ${ result . length } ` ) ;
772771}
772+
773+ export interface CodeActionResult < T extends AstNode = AstNode > extends AsyncDisposable {
774+ /** the document containing the AST */
775+ document : LangiumDocument < T > ;
776+ /** all diagnostics of the validation */
777+ diagnosticsAll : Diagnostic [ ] ;
778+ /** the relevant Diagnostic with the given diagnosticCode, it is expected that the given input has exactly one such diagnostic */
779+ diagnosticRelevant : Diagnostic ;
780+ /** the CodeAction to fix the found relevant problem, it is possible, that there is no such code action */
781+ action ?: CodeAction ;
782+ }
783+
784+ /**
785+ * This is a helper function to easily test code actions (quick-fixes) for validation problems.
786+ * @param services the Langium services for the language with code actions
787+ * @returns A function to easily test a single code action on the given invalid 'input'.
788+ * This function expects, that 'input' contains exactly one validation problem with the given 'diagnosticCode'.
789+ * If 'outputAfterFix' is specified, this functions checks, that the diagnostic comes with a single code action for this validation problem.
790+ * After applying this code action, 'input' is transformed to 'outputAfterFix'.
791+ */
792+ export function testCodeAction < T extends AstNode = AstNode > ( services : LangiumServices ) : ( input : string , diagnosticCode : string , outputAfterFix : string | undefined , options ?: ParseHelperOptions ) => Promise < CodeActionResult < T > > {
793+ const validateHelper = validationHelper < T > ( services ) ;
794+ return async ( input , diagnosticCode , outputAfterFix , options ) => {
795+ // parse + validate
796+ const validationBefore = await validateHelper ( input , options ) ;
797+ const document = validationBefore . document ;
798+ const diagnosticsAll = document . diagnostics ?? [ ] ;
799+ // use only the diagnostics with the given validation code
800+ const diagnosticsRelevant = diagnosticsAll . filter ( d => d . data && 'code' in d . data && d . data . code === diagnosticCode ) ;
801+ // expect exactly one validation with the given code
802+ expectedFunction ( diagnosticsRelevant . length , 1 ) ;
803+ const diagnosticRelevant = diagnosticsRelevant [ 0 ] ;
804+
805+ // check, that the code actions are generated for the selected validation:
806+ // prepare the action provider
807+ const actionProvider = expectTruthy ( services . lsp . CodeActionProvider ) ;
808+ // request the actions for this diagnostic
809+ const currentActions = await actionProvider ! . getCodeActions ( document , {
810+ ...textDocumentParams ( document ) ,
811+ range : diagnosticRelevant . range ,
812+ context : {
813+ diagnostics : diagnosticsRelevant ,
814+ triggerKind : 1 // explicitly triggered by users (or extensions)
815+ }
816+ } ) ;
817+
818+ // evaluate the resulting actions
819+ let action : CodeAction | undefined ;
820+ let validationAfter : ValidationResult | undefined ;
821+ if ( outputAfterFix ) {
822+ // exactly one code action is expected
823+ expectTruthy ( currentActions ) ;
824+ expectTruthy ( Array . isArray ( currentActions ) ) ;
825+ expectedFunction ( currentActions ! . length , 1 ) ;
826+ expectTruthy ( CodeAction . is ( currentActions ! [ 0 ] ) ) ;
827+ action = currentActions ! [ 0 ] as CodeAction ;
828+
829+ // execute the found code action
830+ const edits = expectTruthy ( action . edit ?. changes ! [ document . textDocument . uri ] ) ;
831+ const updatedText = TextDocument . applyEdits ( document . textDocument , edits ! ) ;
832+
833+ // check the result after applying the code action:
834+ // 1st text is updated as expected
835+ expectedFunction ( updatedText , outputAfterFix ) ;
836+ // 2nd the validation diagnostic is gone after applying the code action
837+ validationAfter = await validateHelper ( updatedText , options ) ;
838+ const diagnosticsUpdated = validationAfter . diagnostics . filter ( d => d . data && 'code' in d . data && d . data . code === diagnosticCode ) ;
839+ expectedFunction ( diagnosticsUpdated . length , 0 ) ;
840+ } else {
841+ // no code action is expected
842+ expectFalsy ( currentActions ) ;
843+ }
844+
845+ // collect the data to return
846+ async function dispose ( ) : Promise < void > {
847+ validationBefore . dispose ( ) ;
848+ validationAfter ?. dispose ( ) ;
849+ }
850+ return {
851+ document,
852+ diagnosticsAll,
853+ diagnosticRelevant : diagnosticRelevant ,
854+ action,
855+ dispose : ( ) => dispose ( )
856+ } ;
857+ } ;
858+ }
859+
860+ function expectTruthy < T > ( value : T ) : NonNullable < T > {
861+ if ( value ) {
862+ return value ;
863+ } else {
864+ throw new Error ( ) ;
865+ }
866+ }
867+ function expectFalsy ( value : unknown ) {
868+ if ( value ) {
869+ throw new Error ( ) ;
870+ }
871+ }
0 commit comments