11import {
2- ExtensionContext , languages , Range , TextDocument ,
2+ CancellationToken , ExtensionContext , FormattingOptions , languages , Range , TextDocument ,
33 TextEdit , window , workspace , DecorationOptions ,
44 OutputChannel
55} from "vscode" ;
@@ -11,9 +11,9 @@ import {
1111 CloseAction ,
1212 ErrorHandler
1313} from "vscode-languageclient/node" ;
14- import { resolveVariables } from "./vscode-variables" ;
14+ import { resolveVariables , resolveVariablesForDocument } from "./vscode-variables" ;
1515import { ChildProcessWithoutNullStreams , spawn } from "child_process" ;
16- import { resolve } from "path" ;
16+ import { basename , resolve } from "path" ;
1717import * as fs from "fs" ;
1818
1919let client : LanguageClient ;
@@ -23,6 +23,14 @@ const MAX_SERVER_RESTARTS = 5;
2323const SERVER_RESTART_WINDOW_MS = 60_000 ;
2424const SERVER_RESTART_DELAY_MS = 1_000 ;
2525
26+ type FormatterMode = "process" | "lsp" | "auto" ;
27+
28+ type FormatterInvocation = {
29+ executable : string ;
30+ args : string [ ] ;
31+ workingDirectory : string ;
32+ } ;
33+
2634export async function activate ( context : ExtensionContext ) {
2735 const config = workspace . getConfiguration ( "nattlua" ) ;
2836 let LSPOutput = window . createOutputChannel ( "NattLua LSP Channel" ) ;
@@ -252,6 +260,149 @@ export async function activate(context: ExtensionContext) {
252260 client . sendNotification ( 'nattlua/visibleEditors' , { uris } ) ;
253261 } ;
254262
263+ const getDocumentConfig = ( document ?: TextDocument ) => {
264+ return workspace . getConfiguration ( "nattlua" , document ?. uri ) ;
265+ } ;
266+
267+ const getConfiguredExecutable = ( document ?: TextDocument ) => {
268+ const documentConfig = getDocumentConfig ( document ) ;
269+ return resolveVariablesForDocument ( documentConfig . get < string > ( "executable" ) || "nattlua" , document ) ;
270+ } ;
271+
272+ const getConfiguredWorkingDirectory = ( document ?: TextDocument ) => {
273+ const documentConfig = getDocumentConfig ( document ) ;
274+ return resolveVariablesForDocument ( documentConfig . get < string > ( "workingDirectory" ) || "${workspaceFolder}" , document ) ;
275+ } ;
276+
277+ const getConfiguredArguments = ( document ?: TextDocument ) => {
278+ const documentConfig = getDocumentConfig ( document ) ;
279+ return ( documentConfig . get < string [ ] > ( "arguments" ) || [ ] ) . map ( arg => resolveVariablesForDocument ( arg , document ) ) ;
280+ } ;
281+
282+ const deriveFormatterInvocation = ( document : TextDocument ) : FormatterInvocation | undefined => {
283+ const documentConfig = getDocumentConfig ( document ) ;
284+ const formatterExecutableSetting = documentConfig . get < string > ( "formatterExecutable" ) || "" ;
285+ const formatterArgumentsSetting = documentConfig . get < string [ ] > ( "formatterArguments" ) || [ ] ;
286+ const executable = formatterExecutableSetting . trim ( ) . length > 0
287+ ? resolveVariablesForDocument ( formatterExecutableSetting , document )
288+ : getConfiguredExecutable ( document ) ;
289+ const baseArgs = formatterArgumentsSetting . length > 0
290+ ? formatterArgumentsSetting . map ( arg => resolveVariablesForDocument ( arg , document ) )
291+ : getConfiguredArguments ( document ) ;
292+ const args = [ ...baseArgs ] ;
293+
294+ if ( formatterArgumentsSetting . length === 0 ) {
295+ const lspArgIndex = args . lastIndexOf ( "lsp" ) ;
296+
297+ if ( lspArgIndex >= 0 ) {
298+ args [ lspArgIndex ] = "fmt" ;
299+ } else if ( basename ( executable ) . toLowerCase ( ) . includes ( "nattlua" ) ) {
300+ args . push ( "fmt" ) ;
301+ } else {
302+ return undefined ;
303+ }
304+ }
305+
306+ args . push ( "-" ) ;
307+ args . push ( `--stdin-path=${ JSON . stringify ( document . fileName ) } ` ) ;
308+
309+ return {
310+ executable,
311+ args,
312+ workingDirectory : getConfiguredWorkingDirectory ( document ) ,
313+ } ;
314+ } ;
315+
316+ const runFormatterProcess = (
317+ invocation : FormatterInvocation ,
318+ input : string ,
319+ token : CancellationToken ,
320+ ) : Promise < string > => {
321+ return new Promise ( ( resolvePromise , rejectPromise ) => {
322+ const formatter = spawn ( invocation . executable , invocation . args , {
323+ cwd : invocation . workingDirectory ,
324+ shell : true ,
325+ } ) ;
326+ let stdout = "" ;
327+ let stderr = "" ;
328+ let settled = false ;
329+
330+ const finish = ( callback : ( ) => void ) => {
331+ if ( settled ) {
332+ return ;
333+ }
334+
335+ settled = true ;
336+ cancelSubscription . dispose ( ) ;
337+ callback ( ) ;
338+ } ;
339+
340+ const cancelSubscription = token . onCancellationRequested ( ( ) => {
341+ if ( ! formatter . killed ) {
342+ formatter . kill ( "SIGTERM" ) ;
343+ }
344+ } ) ;
345+
346+ formatter . stdout . setEncoding ( "utf8" ) ;
347+ formatter . stdout . on ( "data" , chunk => {
348+ stdout += chunk ;
349+ } ) ;
350+
351+ formatter . stderr . setEncoding ( "utf8" ) ;
352+ formatter . stderr . on ( "data" , chunk => {
353+ stderr += chunk ;
354+ } ) ;
355+
356+ formatter . on ( "error" , err => {
357+ finish ( ( ) => rejectPromise ( err ) ) ;
358+ } ) ;
359+
360+ formatter . on ( "close" , ( code , signal ) => {
361+ finish ( ( ) => {
362+ if ( token . isCancellationRequested ) {
363+ rejectPromise ( new Error ( "Formatting cancelled" ) ) ;
364+ return ;
365+ }
366+
367+ if ( code === 0 ) {
368+ resolvePromise ( stdout ) ;
369+ return ;
370+ }
371+
372+ const details = stderr . trim ( ) || stdout . trim ( ) || `Formatter exited with code ${ code ?? "null" } signal ${ signal ?? "null" } ` ;
373+ rejectPromise ( new Error ( details ) ) ;
374+ } ) ;
375+ } ) ;
376+
377+ formatter . stdin . on ( "error" , err => {
378+ if ( ( err as NodeJS . ErrnoException ) . code === "EPIPE" ) {
379+ return ;
380+ }
381+
382+ finish ( ( ) => rejectPromise ( err ) ) ;
383+ } ) ;
384+
385+ formatter . stdin . end ( input ) ;
386+ } ) ;
387+ } ;
388+
389+ const formatDocumentWithProcess = async ( document : TextDocument , token : CancellationToken ) => {
390+ const invocation = deriveFormatterInvocation ( document ) ;
391+
392+ if ( ! invocation ) {
393+ throw new Error ( "Could not derive a formatter command from the current NattLua executable and arguments. Configure nattlua.formatterExecutable and nattlua.formatterArguments explicitly." ) ;
394+ }
395+
396+ const original = document . getText ( ) ;
397+ const formatted = await runFormatterProcess ( invocation , original , token ) ;
398+
399+ if ( formatted === original ) {
400+ return [ ] ;
401+ }
402+
403+ return [ TextEdit . replace ( new Range ( document . positionAt ( 0 ) , document . positionAt ( original . length ) ) , formatted ) ] ;
404+ } ;
405+
255406 const executable = resolveVariables ( config . get < string > ( "executable" ) || "nattlua" ) ;
256407 const workingDirectory = resolveVariables ( config . get < string > ( "workingDirectory" ) || "${workspaceFolder}" ) ;
257408 const args = ( config . get < string [ ] > ( "arguments" ) || [ ] ) . map ( arg => resolveVariables ( arg ) ) ;
@@ -340,6 +491,37 @@ export async function activate(context: ExtensionContext) {
340491 configurationSection : "nattlua" ,
341492 } ,
342493 middleware : {
494+ provideDocumentFormattingEdits : async (
495+ document : TextDocument ,
496+ _options : FormattingOptions ,
497+ token : CancellationToken ,
498+ next ,
499+ ) => {
500+ const mode = ( getDocumentConfig ( document ) . get < string > ( "formattingMode" ) || "process" ) as FormatterMode ;
501+
502+ if ( mode === "lsp" ) {
503+ return next ( document , _options , token ) ;
504+ }
505+
506+ try {
507+ return await formatDocumentWithProcess ( document , token ) ;
508+ } catch ( err : any ) {
509+ const message = err ?. message || String ( err ) ;
510+
511+ if ( mode === "auto" ) {
512+ clientOutput . appendLine ( `Process formatter failed for ${ document . fileName } ; falling back to LSP: ${ message } ` ) ;
513+ return next ( document , _options , token ) ;
514+ }
515+
516+ clientOutput . appendLine ( `Process formatter failed for ${ document . fileName } : ${ message } ` ) ;
517+
518+ if ( ! token . isCancellationRequested ) {
519+ window . showErrorMessage ( `NattLua formatter failed: ${ message } ` ) ;
520+ }
521+
522+ return [ ] ;
523+ }
524+ } ,
343525 didOpen : ( document , next ) => {
344526 const visible = isVisibleDocument ( document ) ;
345527
0 commit comments