import { createConnection , TextDocuments , Diagnostic , DiagnosticSeverity , ProposedFeatures , InitializeParams , DidChangeConfigurationNotification , CompletionItem , TextDocumentPositionParams , TextDocumentSyncKind , InitializeResult , Hover , MarkupKind , TextDocumentChangeEvent , DidChangeWatchedFilesParams , HoverParams , DidChangeConfigurationParams , Range , } from "vscode-languageserver" ; import { TextDocument } from "vscode-languageserver-textdocument" ; import { descriptions as directiveDescriptions , } from "./directives" ; import { completions as globalOptionCompletions , descriptions as globalOptionDescriptions , } from "./globalOptions" ; import { getGlobalOptionsPosition , isInGlobalOptions } from "./lexer" ; const connection = createConnection ( ProposedFeatures . all ) ; const documents = new TextDocuments ( TextDocument ) ; let hasConfigurationCapability = false ; let hasWorkspaceFolderCapability = false ; connection . onInitialize ( ( params ) => { let capabilities = params . capabilities ; hasConfigurationCapability = ! ! ( capabilities . workspace && ! ! capabilities . workspace . configuration ) ; hasWorkspaceFolderCapability = ! ! ( capabilities . workspace && ! ! capabilities . workspace . workspaceFolders ) ; const result = { capabilities : { textDocumentSync : TextDocumentSyncKind . Incremental , completionProvider : { resolveProvider : true , } , hoverProvider : true , } , } ; if ( hasWorkspaceFolderCapability ) { result . capabilities . workspace = { workspaceFolders : { supported : true , } , } ; } return result ; } ) ; connection . onInitialized ( ( ) => { if ( hasConfigurationCapability ) { connection . client . register ( DidChangeConfigurationNotification . type , undefined ) ; } if ( hasWorkspaceFolderCapability ) { connection . workspace . onDidChangeWorkspaceFolders ( ( _event ) => { connection . console . log ( "Workspace folder change event received." ) ; } ) ; } } ) ; interface ExampleSettings { maxNumberOfProblems : number ; } ; const defaultSettings = { maxNumberOfProblems : 1000 } ; let globalSettings = defaultSettings ; let documentSettings = new Map ( ) ; connection . onDidChangeConfiguration ( ( change ) => { if ( hasConfigurationCapability ) { documentSettings . clear ( ) ; } else { globalSettings = < ExampleSettings > ( change . settings . languageServerExample || defaultSettings ) ; } documents . all ( ) . forEach ( validateTextDocument ) ; } ) ; documents . onDidClose ( ( e ) => { documentSettings . delete ( e . document . uri ) ; } ) ; documents . onDidChangeContent ( ( change ) => { validateTextDocument ( change . document ) ; } ) ; interface Option { name : string ; range : Range ; } ; async function validateTextDocument ( document ) { const diagnostics = [ ] ; const globalOptionsPosition = getGlobalOptionsPosition ( document ) ; if ( globalOptionsPosition !== null ) { const options = [ ] ; for ( let i = globalOptionsPosition . start . line + 1 ; i < globalOptionsPosition . end . line ; i ++ ) { const line = document . getText ( { start : { line : i , character : 0 } , end : { line : i + 1 , character : 0 } } ) ; const trimmed = line . trim ( ) ; let option ; if ( trimmed . includes ( " " ) ) { const sections = trimmed . split ( " " ) ; option = sections [ 0 ] ; } else { option = trimmed ; } if ( option === "" ) { return ; } const start = line . indexOf ( trimmed . charAt ( 0 ) ) ; options . push ( { name : option , range : { start : { line : i , character : start } , end : { line : i , character : start + option . length } , } , } ) ; } const map = new Map ( ) ; options . forEach ( a => map . set ( a . name , ( map . get ( a . name ) || 0 ) + 1 ) ) ; options . filter ( a => map . get ( a . name ) > 1 ) . forEach ( ( a ) => { if ( a . name === "servers" ) { return ; } diagnostics . push ( { severity : DiagnosticSeverity . Warning , range : a . range , message : `Duplicate global option "${ a . name }"` , source : "caddyfile" , } ) ; } ) ; let start = 0 ; for ( let i = globalOptionsPosition . end . line + 1 ; i < document . lineCount ; i ++ ) { const line = document . getText ( { start : { line : i , character : 0 } , end : { line : i + 1 , character : 0 } , } ) ; if ( line === "{\n" ) { start = i ; continue ; } if ( start === 0 || line !== "}\n" ) { continue ; } diagnostics . push ( { severity : DiagnosticSeverity . Error , message : "Only one global option block is allowed." , source : "caddyfile" , range : { start : { line : start , character : 0 } , end : { line : i + 1 , character : 0 } , } , } ) ; start = 0 ; } } connection . sendDiagnostics ( { uri : document . uri , diagnostics : diagnostics , } ) ; } ; connection . onDidChangeWatchedFiles ( ( _ ) => { connection . console . log ( "We received an file change event" ) ; } ) ; connection . onCompletion ( ( params ) : CompletionItem [ ] => { const document = documents . get ( params . textDocument . uri ) ; if ( document === undefined ) { return [ ] ; } if ( isInGlobalOptions ( document , params ) ) { return globalOptionCompletions ; } return [ ] ; } ) ; connection . onCompletionResolve ( ( item ) : CompletionItem => { return item ; } ) ; connection . onHover ( ( params ) : Hover | null => { const document = documents . get ( params . textDocument . uri ) ; if ( document === undefined ) { return null ; } if ( isInGlobalOptions ( document , params ) ) { const line = document . getText ( { start : { line : params . position . line , character : 0 } , end : { line : params . position . line + 1 , character : 0 } , } ) . trim ( ) ; let option ; if ( line . includes ( " " ) ) { const sections = line . split ( " " ) ; option = sections [ 0 ] ; } else { option = line ; } const description = globalOptionDescriptions [ option ] ; if ( description === undefined ) { return null ; } return { contents : { kind : MarkupKind . Markdown , value : description , } , } ; } const line = document . getText ( { start : { line : params . position . line , character : 0 } , end : { line : params . position . line + 1 , character : 0 } , } ) . trim ( ) ; let directive ; if ( line . includes ( " " ) ) { const sections = line . split ( " " ) ; directive = sections [ 0 ] ; } else { directive = line ; } const description = directiveDescriptions [ directive ] ; if ( description === undefined ) { return null ; } return { contents : { kind : MarkupKind . Markdown , value : description , } , } ; } ) ; documents . listen ( connection ) ; connection . listen ( ) ;