import { HoverProvider , Position , CompletionItem , CancellationToken , TextDocument , Hover , Definition , ProviderResult , Range , Location , ReferenceContext , FormattingOptions , TextEdit , DocumentLink , WorkspaceEdit , SignatureHelp , CodeAction , Command , TextLine , EndOfLine , DocumentLinkProvider , Uri , CodeActionProvider , CodeActionContext , CodeActionKind , SignatureHelpProvider , ReferenceProvider , RenameProvider , DefinitionProvider , workspace , window , CompletionItemProvider , CompletionItemKind , DocumentFormattingEditProvider , Selection } from "vscode" ; import * as utils from "./utils" ; import { Syntaxer } from "./syntaxer" ; import { ErrorInfo , select_line , vsc_config } from './utils' ; import { save_script_project } from './cs-script' ; function isWorkspace ( ) { return workspace . rootPath != undefined ; } function isCssDirective ( document , position ) { return getCssDirective ( document , position ) != null ; } function getCssDirective ( document , position ) { let word_range = document . getWordRangeAtPosition ( position ) ; if ( word_range ) { let word_start = word_range . start ; var word = document . getText ( word_range ) . lines ( ) [ 0 ] ; if ( word . startsWith ( 'css_' ) && word_start . character >= 2 ) { let prefix_range = new Range ( word_start . translate ( 0 , - word_start . character ) , word_start ) ; let prefix = document . getText ( prefix_range ) . lines ( ) [ 0 ] ; let directive = ( prefix + word ) . trim ( ) ; if ( directive . startsWith ( '//css_' ) ) return directive ; } } return null ! ; } function trimWordDelimiters ( word ) { if ( word . startsWith ( '//' ) ) return word . substring ( 2 ) ; else return word ; } export class CSScriptHoverProvider implements HoverProvider { public provideHover ( document , position , token ) { let result ; let is_workspace = isWorkspace ( ) ; let is_css_directive = isCssDirective ( document , position ) ; if ( ! is_workspace || is_css_directive ) { return new Promise ( ( resolve , reject ) => Syntaxer . getTooltip ( document . getText ( ) , document . fileName , document . offsetAt ( position ) , ( data ) => { if ( ! data . startsWith ( "<null>" ) && ! data . startsWith ( "<error>" ) ) { result = new Hover ( data ) ; } resolve ( result ) ; } ) ) ; } return null ! ; } } export function process_snippet_cursor_placeholders ( startLine , endLine ) { let editor = window . activeTextEditor ; for ( let index = startLine ; index < endLine ; index ++ ) { let line_text = editor ! . document . lineAt ( index ) . text ; let cursor_pos = line_text . indexOf ( "$0" ) ; if ( cursor_pos != - 1 ) { editor ! . edit ( editBuilder => editBuilder . replace ( new Range ( index , cursor_pos , index , cursor_pos + 2 ) , '' ) ) . then ( ( ) => editor ! . selection = new Selection ( index , cursor_pos , index , cursor_pos ) ) ; break ; } } } export class CSScriptCompletionItemProvider implements CompletionItemProvider { public provideCompletionItems ( document , position , token ) { let items = [ ] ; let is_workspace = isWorkspace ( ) ; let is_css_directive = isCssDirective ( document , position ) ; let eol = document . eol == EndOfLine . CRLF ? "\r\n" : "\n" ; if ( ! is_workspace || is_css_directive ) { return new Promise ( ( resolve , reject ) => Syntaxer . getCompletions ( document . getText ( ) , document . fileName , document . offsetAt ( position ) , ( data ) => { if ( ! data . startsWith ( "<null>" ) && ! data . startsWith ( "<error>" ) ) { let line_indent = utils . get_line_indent ( document . lineAt ( position . line ) . text ) ; let lines = data . lines ( ) ; lines . forEach ( line => { let parts = line . replace ( "$|$" , "$0" ) . split ( "\t" ) ; let info = parts [ 1 ] . split ( "|" ) ; let completionText = utils . css_unescape_linebreaks ( info [ 1 ] , eol ) ; completionText = trimWordDelimiters ( completionText ) ; let pattern = new RegExp ( "\\n\\s{" + line_indent + "}" , 'g' ) ; completionText = completionText . replace ( pattern , "\n" ) ; let doc ; if ( info . length > 2 ) doc = utils . css_unescape_linebreaks ( info [ 2 ] , eol ) ; let command ; let memberKind ; switch ( info [ 0 ] ) { case "method" : memberKind = CompletionItemKind . Method ; break ; case "extension_method" : memberKind = CompletionItemKind . Method ; break ; case "constructor" : memberKind = CompletionItemKind . Constructor ; break ; case "field" : memberKind = CompletionItemKind . Field ; break ; case "property" : memberKind = CompletionItemKind . Property ; break ; case "event" : { command = { title : "" , command : "cs-script.on_completion_accepted" , arguments : [ ( ) => { process_snippet_cursor_placeholders ( position . line , position . line + completionText . lines ( ) . length ) ; } ] } ; memberKind = CompletionItemKind . Event ; break ; } default : memberKind = CompletionItemKind . Text ; break ; } items . push ( { label : parts [ 0 ] , kind : memberKind , documentation : doc ! , insertText : completionText , command : command ! , sortText : '01' } ) ; } ) ; } resolve ( items ) ; } ) ) ; } return null ! ; } } let syntaxer_navigate_selectedLine = - 1 ; window . onDidChangeActiveTextEditor ( editor => { if ( syntaxer_navigate_selectedLine != - 1 ) { let line = syntaxer_navigate_selectedLine ; setTimeout ( ( ) => { select_line ( line ) ; } , 100 ) ; syntaxer_navigate_selectedLine = - 1 ; } } ) ; export class CSScriptDocFormattingProvider implements DocumentFormattingEditProvider { public provideDocumentFormattingEdits ( document , options , token ) { let result = [ ] ; let is_workspace = isWorkspace ( ) ; let enabled = vsc_config . get ( "cs-script.enable_code_formatting" , true ) ; if ( ! is_workspace && enabled ) return new Promise ( ( resolve , reject ) => { let editor = window . activeTextEditor ; let position = editor ! . selection . start ; Syntaxer . doDocFormat ( document . getText ( ) , document . fileName , document . offsetAt ( position ) , ( data ) => { if ( ! data . startsWith ( "<null>" ) && ! data . startsWith ( "<error>" ) ) { let info = data . lines ( 2 ) ; let newText = data . substring ( info [ 0 ] . length + 1 ) ; let wholeDoc = new Range ( document . positionAt ( 0 ) , document . positionAt ( document . getText ( ) . length ) ) ; result . push ( new TextEdit ( wholeDoc , newText ) ) ; } resolve ( result ) ; } ) ; } ) ; else return null ; } } export class CSScriptLinkProvider implements DocumentLinkProvider { private _linkPattern = /.*?\(.*?\):/g ; public provideDocumentLinks ( document , token ) { let result = [ ] ; let enabled = vsc_config . get ( "cs-script.decorate_file_links_in_output" , true ) ; if ( enabled ) return new Promise ( ( resolve , reject ) => { const text = document . getText ( ) ; let match ; while ( ( match = this . _linkPattern . exec ( text ) ! ) ) { let matchText = match [ 0 ] ; let info = ErrorInfo . parse ( matchText ) ; let targetPosUri = Uri . file ( info . file ) . with ( { fragment : `${ 1 + info . range . start . line }` } ) ; let linkEnd = document . positionAt ( this . _linkPattern . lastIndex - 1 ) ; let linkStart = document . positionAt ( this . _linkPattern . lastIndex - 1 - matchText . length ) ; result . push ( new DocumentLink ( new Range ( linkStart , linkEnd ) , targetPosUri ) ) ; } resolve ( result ) ; } ) ; else return null ; } } export class CSScriptCodeActionProvider implements CodeActionProvider { public provideCodeActions ( document , range , context , token ) { let result = [ ] ; let is_workspace = isWorkspace ( ) ; if ( ! is_workspace ) { return new Promise ( ( resolve , reject ) => { let offset = document . offsetAt ( range . end ) - 1 ; let word_range = document . getWordRangeAtPosition ( document . positionAt ( offset ) ) ; let word = document . getText ( word_range ) ; if ( word != "" ) Syntaxer . suggestUsings ( document . getText ( ) , document . fileName , word , ( data ) => { if ( ! data . startsWith ( "<null>" ) && ! data . startsWith ( "<error>" ) ) { let eol = document . eol == EndOfLine . CRLF ? "\r\n" : "\n" ; let lastCssDirective ; let usings = [ ] ; for ( let index = 0 ; index <= range . start . line ; index ++ ) { let line = document . lineAt ( index ) ; if ( line . text . trim ( ) . startsWith ( "using " ) ) usings . push ( line ) ; else if ( line . text . trim ( ) . startsWith ( "//css_" ) ) lastCssDirective = line ; } function find_using_placement ( text ) { if ( usings . any ( ) ) return usings . firstOrDefault < TextLine > ( ) . range . start ; else return lastCssDirective ? new Position ( lastCssDirective . range . end . line + 1 , 0 ) : new Position ( 0 , 0 ) ; } let new_usings = [ ] ; let new_replacements = [ ] ; let lines = data . lines ( ) ; let already_handled = false ; lines . forEach ( line => { if ( ! already_handled ) { let insert_action ; let replace_action ; let new_using = `using ${ line };` ; let already_present = usings . any < TextLine > ( x => x . text == new_using ) ; if ( ! already_present ) { let insertion_pos = find_using_placement ( new_using ) ; if ( insertion_pos ) { insert_action = new CodeAction ( `Add "${ new_using }"` ) ; insert_action . kind = CodeActionKind . RefactorRewrite ; insert_action . edit = new WorkspaceEdit ( ) ; insert_action . edit . insert ( Uri . file ( document . fileName ) , insertion_pos , new_using + eol ) ; } } else already_handled = true ; let replacement_word = `${ line }.${ word }` ; let already_fully_specified = false ; let start_of_expression = word_range ?. end . character ! - replacement_word . length ; if ( start_of_expression >= 0 ) { let whole_word_range = new Range ( new Position ( word_range ! . start . line , start_of_expression ) , word_range ! . end ) ; let whole_word = document . getText ( whole_word_range ) ; if ( whole_word == replacement_word ) { already_fully_specified = true ; already_handled = true ; } } if ( ! already_fully_specified && ! already_present ) { replace_action = new CodeAction ( `Change "${ word }" to "${ line }.${ word }"` ) ; replace_action . kind = CodeActionKind . RefactorRewrite ; replace_action . edit = new WorkspaceEdit ( ) ; replace_action . edit . replace ( Uri . file ( document . fileName ) , word_range ! , replacement_word ) ; } if ( insert_action ! != undefined && replace_action ! != undefined ) { new_usings . push ( insert_action ) ; new_replacements . push ( replace_action ) ; } } } ) ; if ( ! already_handled ) { new_usings . forEach ( x => result . push ( x ) ) ; let enable_grouping = vsc_config . get ( "cs-script.group_suggested_code_actions" , true ) ; if ( enable_grouping ) result . push ( new CodeAction ( `--------------------------` ) ) ; new_replacements . forEach ( x => result . push ( x ) ) ; } } resolve ( result ) ; } ) ; } ) ; } return null ; } } export class CSScriptSignatureHelpProvider implements SignatureHelpProvider { public provideSignatureHelp ( document , position , token ) { let result ; let is_workspace = isWorkspace ( ) ; if ( ! is_workspace ) { let eol = document . eol == EndOfLine . CRLF ? "\r\n" : "\n" ; return new Promise ( ( resolve , reject ) => Syntaxer . getSignatureHelp ( document . getText ( ) , document . fileName , document . offsetAt ( position ) , ( data ) => { if ( ! data . startsWith ( "<null>" ) && ! data . startsWith ( "<error>" ) ) { let lines = data . lines ( ) ; let bestMatch = lines [ 0 ] . split ( '/' ) ; let bestSigIndex = Number ( bestMatch [ 0 ] ) ; let bestParamIndex = Number ( bestMatch [ 1 ] ) ; result = new SignatureHelp ( ) ; for ( let i = 1 ; i < lines . length ; i ++ ) { let sig_info = utils . css_unescape_linebreaks ( lines [ i ] , eol ) ; let sig = utils . toSignatureInfo ( sig_info ) ; result . signatures . push ( sig ) ; } result . activeSignature = bestSigIndex ; if ( bestSigIndex != - 1 ) { result . activeParameter = bestParamIndex ; } } resolve ( result ) ; } ) ) ; } return null ; } } export class CSScriptReferenceProvider implements ReferenceProvider { public provideReferences ( document , position , context , token ) { let result = [ ] ; let is_workspace = isWorkspace ( ) ; if ( ! is_workspace ) { return new Promise ( ( resolve , reject ) => Syntaxer . getRefrences ( document . getText ( ) , document . fileName , document . offsetAt ( position ) , ( data ) => { if ( ! data . startsWith ( "<null>" ) && ! data . startsWith ( "<error>" ) ) { let lines = data . lines ( ) ; lines . forEach ( line => { let info = ErrorInfo . parse ( line ) ; result . push ( new Location ( Uri . file ( info . file ) , info . range ) ) ; } ) ; } resolve ( result ) ; } ) ) ; } return null ; } } export class CSScriptRenameProvider implements RenameProvider { public provideRenameEdits ( document , position , newName , token ) { let is_workspace = isWorkspace ( ) ; if ( ! is_workspace ) { let word = document . getWordRangeAtPosition ( position ) ; let word_width = word ! . end . character - word ! . start . character ; return new Promise ( ( resolve , reject ) => Syntaxer . getRenamingInfo ( document . getText ( ) , document . fileName , document . offsetAt ( position ) , ( data ) => { let result = new WorkspaceEdit ( ) ; try { if ( ! data . startsWith ( "<null>" ) && ! data . startsWith ( "<error>" ) ) { var changes = { } ; let lines = data . lines ( ) ; lines . forEach ( line => { let info = ErrorInfo . parse ( line ) ; let range = new Range ( info . range . start , new Position ( info . range . start . line , info . range . start . character + word_width ) ) ; let edits = changes [ info . file ] ; if ( ! edits ) { edits = [ ] ; changes [ info . file ] = edits ; } edits . push ( new TextEdit ( range , newName ) ) ; } ) ; for ( let file in changes ) { let edits = changes [ file ] ; let uri = Uri . file ( utils . clear_temp_file_suffixes ( file ) ) ; result . set ( uri , edits ) ; } } } catch { } resolve ( result ) ; setTimeout ( ( ) => save_script_project ( false ) , 700 ) ; } ) ) ; } return null ; } } export class CSScriptDefinitionProvider implements DefinitionProvider { public provideDefinition ( document , position , token ) { syntaxer_navigate_selectedLine = - 1 ; let result ; let is_workspace = isWorkspace ( ) ; let is_css_directive = isCssDirective ( document , position ) ; if ( ! is_workspace || is_css_directive ) { return new Promise ( ( resolve , reject ) => Syntaxer . getDefinition ( document . getText ( ) , document . fileName , document . offsetAt ( position ) , ( data ) => { if ( ! data . startsWith ( "<null>" ) && ! data . startsWith ( "<error>" ) ) { let lines = data . lines ( ) ; let file = lines [ 0 ] . substr ( 'file:' . length ) ; let line = Number ( lines [ 1 ] . substr ( 'line:' . length ) ) - 1 ; if ( file . length > 0 ) { syntaxer_navigate_selectedLine = line ; result = new Location ( Uri . file ( file ) , new Range ( new Position ( line , 0 ) , new Position ( line , 0 ) ) ) ; if ( file . toLowerCase ( ) == document . fileName . toLowerCase ( ) ) { setTimeout ( ( ) => select_line ( syntaxer_navigate_selectedLine ) , 30 ) ; } } } resolve ( result ) ; } ) ) ; } return null ; } }