1- import { ChildProcess } from 'child_process' ;
2- import * as path from 'path' ;
31import * as vscode from 'vscode' ;
42import { LANG_TLAPLUS , LANG_TLAPLUS_CFG } from '../common' ;
5- import { applyDCollection } from '../diagnostic' ;
6- import { ModelCheckResult , ModelCheckResultSource , SpecFiles } from '../model/check' ;
7- import { ToolOutputChannel } from '../outputChannels' ;
8- import { saveStreamToFile } from '../outputSaver' ;
9- import {
10- revealEmptyCheckResultView ,
11- revealLastCheckResultView ,
12- updateCheckResultView
13- } from '../panels/checkResultView' ;
14- import { TlcModelCheckerStdoutParser } from '../parsers/tlc' ;
15- import { runTlc , stopProcess } from '../tla2tools' ;
16- import { ModelResolveMode , resolveModelForUri } from './modelResolver' ;
17- import { TlcCoverageDecorationProvider } from '../tlcCoverage' ;
18- import { tlcTraceToPuml } from '../generators/tlcTraceToPuml' ;
19- import { showSequenceDiagramFromPuml } from '../panels/sequenceDiagramView' ;
3+ import { SpecFiles } from '../model/check' ;
4+ import { ExtensionRuntime } from '../runtime/extensionRuntime' ;
5+ import { CheckSession } from '../services/checkService' ;
6+ import { CheckResultViewController } from '../panels/checkResultView' ;
7+ import { ModelResolveMode } from './modelResolver' ;
208
219export const CMD_CHECK_MODEL_RUN = 'tlaplus.model.check.run' ;
2210export const CMD_CHECK_MODEL_RUN_AGAIN = 'tlaplus.model.check.runAgain' ;
@@ -27,64 +15,51 @@ export const CMD_SHOW_TLC_OUTPUT = 'tlaplus.showTlcOutput';
2715export const CTX_TLC_RUNNING = 'tlaplus.tlc.isRunning' ;
2816export const CTX_TLC_CAN_RUN_AGAIN = 'tlaplus.tlc.canRunAgain' ;
2917
30- const CFG_CREATE_OUT_FILES = 'tlaplus.tlc.modelChecker.createOutFiles' ;
31- const CFG_SEQ_DIAGRAM_TRACE_VAR = 'tlaplus.tlc.sequenceDiagram.traceVariable' ;
32- let checkProcess : ChildProcess | undefined ;
33- let lastCheckFiles : SpecFiles | undefined ;
34- let coverageProvider : TlcCoverageDecorationProvider | undefined ;
35- const statusBarItem = vscode . window . createStatusBarItem ( vscode . StatusBarAlignment . Left , 0 ) ;
36- export const outChannel = new ToolOutputChannel ( 'TLC' , mapTlcOutputLine ) ;
37-
38- class CheckResultHolder {
39- checkResult : ModelCheckResult | undefined ;
40- }
41-
42- /**
43- * Sets the coverage provider to be used for visualization.
44- */
45- export function setCoverageProvider ( provider : TlcCoverageDecorationProvider ) : void {
46- coverageProvider = provider ;
47- }
48-
49- /**
50- * Runs TLC on a TLA+ specification.
51- */
5218export async function checkModel (
5319 fileUri : vscode . Uri | undefined ,
54- diagnostic : vscode . DiagnosticCollection ,
55- extContext : vscode . ExtensionContext
20+ runtime : ExtensionRuntime ,
21+ extContext : vscode . ExtensionContext ,
22+ checkResultView : CheckResultViewController
5623) : Promise < void > {
57- const uri = fileUri ? fileUri : getActiveEditorFileUri ( extContext ) ;
24+ const uri = fileUri ? fileUri : getActiveEditorFileUri ( ) ;
5825 if ( ! uri ) {
5926 return ;
6027 }
6128
62- const specFiles = await getSpecFiles ( uri , true ) ;
29+ const specFiles = await runtime . checkService . resolveSpecFiles ( uri , true ) ;
6330 if ( ! specFiles ) {
6431 return ;
6532 }
66- await doCheckModel ( specFiles , true , extContext , diagnostic , true ) ;
33+
34+ await runCheckSession ( specFiles , runtime , extContext , checkResultView , {
35+ showOptionsPrompt : true ,
36+ showFullOutput : true ,
37+ } , true ) ;
6738}
6839
6940export async function runLastCheckAgain (
70- diagnostic : vscode . DiagnosticCollection ,
71- extContext : vscode . ExtensionContext
41+ runtime : ExtensionRuntime ,
42+ extContext : vscode . ExtensionContext ,
43+ checkResultView : CheckResultViewController
7244) : Promise < void > {
45+ const lastCheckFiles = runtime . checkService . sessions . lastSpecFiles ( ) ;
7346 if ( ! lastCheckFiles ) {
7447 vscode . window . showWarningMessage ( 'No last check to run' ) ;
7548 return ;
7649 }
77- if ( ! canRunTlc ( extContext ) ) {
78- return ;
79- }
80- await doCheckModel ( lastCheckFiles , true , extContext , diagnostic , false ) ;
50+
51+ await runCheckSession ( lastCheckFiles , runtime , extContext , checkResultView , {
52+ showOptionsPrompt : false ,
53+ showFullOutput : true ,
54+ } , true ) ;
8155}
8256
8357export async function checkModelCustom (
84- diagnostic : vscode . DiagnosticCollection ,
85- extContext : vscode . ExtensionContext
58+ runtime : ExtensionRuntime ,
59+ extContext : vscode . ExtensionContext ,
60+ checkResultView : CheckResultViewController ,
8661) : Promise < void > {
87- const editor = getEditorIfCanRunTlc ( extContext ) ;
62+ const editor = getEditorIfCanRunTlc ( ) ;
8863 if ( ! editor ) {
8964 return ;
9065 }
@@ -94,57 +69,40 @@ export async function checkModelCustom(
9469 'File in the active editor is not a .tla or .cfg, it cannot be checked as a model' ) ;
9570 return ;
9671 }
97- const specFiles = await getSpecFiles ( doc . uri , true , true , 'customPick' ) ;
72+ const specFiles = await runtime . checkService . resolveSpecFiles ( doc . uri , true , true , 'customPick' ) ;
9873 if ( ! specFiles ) {
9974 return ;
10075 }
101- await doCheckModel ( specFiles , true , extContext , diagnostic , true ) ;
76+
77+ await runCheckSession ( specFiles , runtime , extContext , checkResultView , {
78+ showOptionsPrompt : true ,
79+ showFullOutput : true ,
80+ } , true ) ;
10281}
10382
104- /**
105- * Reveals model checking view panel.
106- */
107- export function displayModelChecking ( extContext : vscode . ExtensionContext ) : void {
108- revealLastCheckResultView ( extContext ) ;
83+ export function displayModelChecking ( checkResultView : CheckResultViewController ) : void {
84+ checkResultView . revealLastResult ( ) ;
10985}
11086
111- /**
112- * Stops the current model checking process.
113- */
11487export function stopModelChecking (
88+ runtime : ExtensionRuntime ,
11589 terminateLastRun : ( lastSpecFiles : SpecFiles | undefined ) => boolean =
116- ( ) : boolean => { return true ; } ,
117- silent : boolean = false
90+ ( ) : boolean => true ,
91+ silent = false
11892) : void {
119- if ( checkProcess && terminateLastRun ( lastCheckFiles ) ) {
120- stopProcess ( checkProcess ) ;
121- } else if ( ! silent ) {
93+ const stopped = runtime . checkService . stopLatestSession (
94+ ( session ) => terminateLastRun ( session . specFiles )
95+ ) ;
96+ if ( ! stopped && ! silent ) {
12297 vscode . window . showInformationMessage ( "There're no currently running model checking processes" ) ;
12398 }
12499}
125100
126- export function showTlcOutput ( ) : void {
127- outChannel . revealWindow ( ) ;
128- }
129-
130- function getActiveEditorFileUri ( extContext : vscode . ExtensionContext ) : vscode . Uri | undefined {
131- const editor = getEditorIfCanRunTlc ( extContext ) ;
132- if ( ! editor ) {
133- return undefined ;
134- }
135- const doc = editor . document ;
136- if ( doc . languageId !== LANG_TLAPLUS && doc . languageId !== LANG_TLAPLUS_CFG ) {
137- vscode . window . showWarningMessage (
138- 'File in the active editor is not a .tla or .cfg file, it cannot be checked as a model' ) ;
139- return undefined ;
140- }
141- return doc . uri ;
101+ export function showTlcOutput ( runtime : ExtensionRuntime ) : void {
102+ runtime . checkService . revealOutput ( ) ;
142103}
143104
144- export function getEditorIfCanRunTlc ( extContext : vscode . ExtensionContext ) : vscode . TextEditor | undefined {
145- if ( ! canRunTlc ( extContext ) ) {
146- return undefined ;
147- }
105+ export function getEditorIfCanRunTlc ( ) : vscode . TextEditor | undefined {
148106 const editor = vscode . window . activeTextEditor ;
149107 if ( ! editor ) {
150108 vscode . window . showWarningMessage ( 'No editor is active, cannot find a TLA+ model to check' ) ;
@@ -153,166 +111,55 @@ export function getEditorIfCanRunTlc(extContext: vscode.ExtensionContext): vscod
153111 return editor ;
154112}
155113
156- function canRunTlc ( extContext : vscode . ExtensionContext ) : boolean {
157- if ( checkProcess ) {
158- vscode . window . showWarningMessage (
159- 'Another model checking process is currently running' ,
160- 'Show currently running process'
161- ) . then ( ( ) => revealLastCheckResultView ( extContext ) ) ;
162- return false ;
163- }
164- return true ;
165- }
166-
167- export async function doCheckModel (
168- specFiles : SpecFiles ,
169- showCheckResultView : boolean ,
170- extContext : vscode . ExtensionContext ,
171- diagnostic : vscode . DiagnosticCollection ,
172- showOptionsPrompt : boolean ,
173- extraOpts : string [ ] = [ ] ,
174- debuggerPortCallback ?: ( port ?: number ) => void
175- ) : Promise < ModelCheckResult | undefined > {
176- try {
177- const procInfo = await runTlc (
178- specFiles . tlaFilePath , specFiles . cfgFilePath , showOptionsPrompt , extraOpts ) ;
179- if ( procInfo === undefined ) {
180- // Command cancelled by user, make sure UI state is reset
181- vscode . commands . executeCommand ( 'setContext' , CTX_TLC_CAN_RUN_AGAIN , ! ! lastCheckFiles ) ;
182- updateStatusBarItem ( false , lastCheckFiles ) ;
183- return undefined ;
184- }
185- lastCheckFiles = specFiles ;
186- vscode . commands . executeCommand ( 'setContext' , CTX_TLC_CAN_RUN_AGAIN , true ) ;
187- updateStatusBarItem ( true , specFiles ) ;
188- outChannel . bindTo ( procInfo ) ;
189- checkProcess = procInfo . process ;
190- checkProcess . on ( 'close' , ( ) => {
191- checkProcess = undefined ;
192- updateStatusBarItem ( false , lastCheckFiles ) ;
193- } ) ;
194- if ( showCheckResultView ) {
195- attachFileSaver ( specFiles , checkProcess ) ;
196- revealEmptyCheckResultView ( extContext ) ;
197- }
198- const resultHolder = new CheckResultHolder ( ) ;
199- const checkResultCallback = ( checkResult : ModelCheckResult ) => {
200- resultHolder . checkResult = checkResult ;
201- if ( showCheckResultView ) {
202- updateCheckResultView ( checkResult ) ;
203- }
204-
205- // Update coverage visualization
206- if ( coverageProvider && checkResult . coverageStat . length > 0 ) {
207- // Get total distinct states from the last entry in initialStatesStat
208- let totalDistinctStates = 0 ;
209- if ( checkResult . initialStatesStat . length > 0 ) {
210- const lastStat = checkResult . initialStatesStat [ checkResult . initialStatesStat . length - 1 ] ;
211- totalDistinctStates = lastStat . distinct ;
212- }
213- coverageProvider . updateCoverage ( checkResult . coverageStat , totalDistinctStates ) ;
214- }
215- } ;
216- // Accumulate raw stdout for sequence-diagram generation
217- const rawChunks : string [ ] = [ ] ;
218- if ( checkProcess . stdout ) {
219- checkProcess . stdout . on ( 'data' , ( chunk : Buffer | string ) => {
220- rawChunks . push ( String ( chunk ) ) ;
221- } ) ;
222- }
223- const stdoutParser = new TlcModelCheckerStdoutParser (
224- ModelCheckResultSource . Process ,
225- checkProcess . stdout ,
226- specFiles ,
227- true ,
228- checkResultCallback ,
229- debuggerPortCallback
230- ) ;
231- const dCol = await stdoutParser . readAll ( ) ;
232- applyDCollection ( dCol , diagnostic ) ;
233- // Auto-generate PlantUML sequence diagram from TLC error trace
234- tryGenerateSequenceDiagram ( rawChunks . join ( '' ) , specFiles , extContext ) ;
235- return resultHolder . checkResult ;
236- } catch ( err ) {
237- statusBarItem . hide ( ) ;
238- vscode . window . showErrorMessage ( `Error checking model: ${ err } ` ) ;
239- }
240- return undefined ;
241- }
242-
243- function attachFileSaver ( specFiles : SpecFiles , proc : ChildProcess ) {
244- const createOutFiles = vscode . workspace . getConfiguration ( ) . get < boolean > ( CFG_CREATE_OUT_FILES ) ;
245- if ( typeof ( createOutFiles ) === 'undefined' || createOutFiles ) {
246- const outFilePath = path . join ( specFiles . outputDir , `${ specFiles . modelName } .out` ) ;
247- saveStreamToFile ( proc . stdout , outFilePath ) ;
248- }
249- }
250-
251- /**
252- * Finds all files that needed to run model check.
253- */
254- export async function getSpecFiles (
114+ export async function resolveSpecFiles (
115+ runtime : ExtensionRuntime ,
255116 fileUri : vscode . Uri ,
256117 warn = true ,
257118 interactive = true ,
258119 mode : ModelResolveMode = 'adjacent'
259120) : Promise < SpecFiles | undefined > {
260- const resolved = await resolveModelForUri ( fileUri , warn , interactive , mode ) ;
261- if ( ! resolved ) {
262- return undefined ;
263- }
264- return new SpecFiles (
265- resolved . tlaPath ,
266- resolved . cfgPath ,
267- resolved . modelName ,
268- resolved . outputDir
269- ) ;
121+ return runtime . checkService . resolveSpecFiles ( fileUri , warn , interactive , mode ) ;
270122}
271123
272- function updateStatusBarItem ( active : boolean , specFiles : SpecFiles | undefined ) {
273- statusBarItem . text = 'TLC' + ( active ?
274- ( specFiles === undefined ? '' : ` (Model: ${ specFiles . modelName } )` )
275- + ' $(gear~spin)' : '' ) ;
276- statusBarItem . tooltip = 'TLA+ model checking' + ( active ?
277- ( specFiles === undefined ? '' : ` of model ${ specFiles . modelName } ` )
278- + ' is running' : ' result' ) ;
279- statusBarItem . command = CMD_CHECK_MODEL_DISPLAY ;
280- statusBarItem . show ( ) ;
281- vscode . commands . executeCommand ( 'setContext' , CTX_TLC_RUNNING , active ) ;
282- }
124+ export async function runCheckSession (
125+ specFiles : SpecFiles ,
126+ runtime : ExtensionRuntime ,
127+ extContext : vscode . ExtensionContext ,
128+ checkResultView : CheckResultViewController ,
129+ request : {
130+ showOptionsPrompt : boolean ;
131+ showFullOutput ?: boolean ;
132+ extraOpts ?: string [ ] ;
133+ debuggerPortCallback ?: ( port ?: number ) => void ;
134+ } ,
135+ showCheckResultView : boolean ,
136+ ) : Promise < CheckSession | undefined > {
137+ if ( showCheckResultView ) {
138+ checkResultView . revealEmpty ( ) ;
139+ }
283140
141+ const session = await runtime . checkService . startSession ( specFiles , request ) ;
142+ if ( ! session ) {
143+ return undefined ;
144+ }
284145
285- export function mapTlcOutputLine ( line : string ) : string | undefined {
286- if ( line === '' ) {
287- return line ;
146+ await session . completion ;
147+ if ( showCheckResultView ) {
148+ await runtime . checkService . maybeGenerateSequenceDiagram ( session , extContext ) ;
288149 }
289- const cleanLine = line . replace ( / @ ! @ ! @ ( S T A R T | E N D ) M S G \d + ( : \d + ) ? @ ! @ ! @ / g, '' ) ;
290- return cleanLine === '' ? undefined : cleanLine ;
150+ return session ;
291151}
292152
293- /**
294- * Attempts to generate a PlantUML sequence diagram from raw TLC output.
295- * Errors are logged to the TLC output channel, never thrown.
296- */
297- function tryGenerateSequenceDiagram (
298- rawOutput : string ,
299- specFiles : SpecFiles ,
300- extContext : vscode . ExtensionContext ,
301- ) : void {
302- try {
303- const traceVar = vscode . workspace . getConfiguration ( )
304- . get < string > ( CFG_SEQ_DIAGRAM_TRACE_VAR ) ?? '_seqDiagramTrace' ;
305- const puml = tlcTraceToPuml ( rawOutput , {
306- traceVariable : traceVar ,
307- title : specFiles . modelName ,
308- } ) ;
309- if ( puml ) {
310- showSequenceDiagramFromPuml ( puml , extContext , specFiles . modelName )
311- . catch ( err => {
312- outChannel . appendLine ( `[Sequence Diagram] Failed to open editor: ${ err } ` ) ;
313- } ) ;
314- }
315- } catch ( err ) {
316- outChannel . appendLine ( `[Sequence Diagram] Failed to generate: ${ err } ` ) ;
153+ function getActiveEditorFileUri ( ) : vscode . Uri | undefined {
154+ const editor = getEditorIfCanRunTlc ( ) ;
155+ if ( ! editor ) {
156+ return undefined ;
157+ }
158+ const doc = editor . document ;
159+ if ( doc . languageId !== LANG_TLAPLUS && doc . languageId !== LANG_TLAPLUS_CFG ) {
160+ vscode . window . showWarningMessage (
161+ 'File in the active editor is not a .tla or .cfg file, it cannot be checked as a model' ) ;
162+ return undefined ;
317163 }
164+ return doc . uri ;
318165}
0 commit comments