├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── keymaps └── autocomplete-fortran.cson ├── lib ├── autocomplete-fortran.coffee └── fortran-provider.coffee ├── menus └── autocomplete-fortran.cson ├── package.json └── python └── parse_fortran.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.9.7 2 | 3 | **Final release, package is now deprecated.** 4 | 5 | * Added deprecation warning to README 6 | 7 | ## 0.9.6 8 | 9 | * Added package deprecation warning to startup 10 | 11 | ## 0.9.5 12 | 13 | ### Fixes 14 | * Fix bug with colon separators in USE statements- fixes issue [#10](https://github.com/hansec/autocomplete-fortran/issues/10) 15 | 16 | ## 0.9.4 17 | 18 | ### Fixes 19 | * Fix bug with Windows line endings in fixed format files- fixes issue [#8](https://github.com/hansec/autocomplete-fortran/issues/8) 20 | 21 | ## 0.9.3 22 | 23 | ### Improvements 24 | * Catch errors related to incomplete or invalid Python paths 25 | 26 | ## 0.9.2 27 | 28 | ### Fixes 29 | * Fix bug with print statements in Python 3- fixes issue [#5](https://github.com/hansec/autocomplete-fortran/issues/5) 30 | 31 | ## 0.9.1 32 | 33 | ### Fixes 34 | * Fix bug with suggestions when type name and member name are the same- fixes issue [#4](https://github.com/hansec/autocomplete-fortran/issues/4) 35 | 36 | ## 0.9.0 37 | 38 | ### Improvements 39 | * Add support for generating and using external index files 40 | * Use snippet package for subroutine/function arguments in suggestions 41 | * Add context filtering for CALL statements 42 | * Add context filtering for DEALLOCATE/NULLIFY statements 43 | 44 | ## 0.8.2 45 | 46 | ### Fixes 47 | * Fix bug with missing suggestions when uppercase fields are present in a user-defined type 48 | 49 | ## 0.8.1 50 | 51 | ### Fixes 52 | * Fix fully-qualified object name construction for deep nesting levels 53 | * Improve error handling for parser failures 54 | 55 | ## 0.8.0 56 | 57 | ### Improvements 58 | * Preserve completion case in suggestions 59 | 60 | ### Fixes 61 | * Fix suggestions for functions with variable-style definitions (erroneous comma in return type) 62 | 63 | ## 0.7.1 64 | 65 | ### Improvements 66 | * Add support for additional file extensions to match the language-fortran package- fixes issue [#3](https://github.com/hansec/autocomplete-fortran/issues/3) 67 | 68 | ## 0.7.0 69 | 70 | ### Improvements 71 | * Improve parsing speed during initial indexing and full rebuilds (initial indexing should now be almost instant) 72 | * Detect and handle line continuation with context based suggestion filtering (ex. USE statements) 73 | 74 | ## 0.6.1 75 | 76 | ### Fixes 77 | * Catch invalid or inaccessible module directory paths- fixes issue [#2](https://github.com/hansec/autocomplete-fortran/issues/2) 78 | * Fix parsing errors when a file is saved while adding a new scope 79 | 80 | ## 0.6.0 81 | 82 | ### Improvements 83 | * Provide array dimensions in suggestions 84 | * Add context filtering when only user-defined type names are valid 85 | 86 | ## 0.5.0 87 | 88 | ### Improvements 89 | * Track child classes and update fields when parent class is updated 90 | * Trigger reparsing when a buffer is saved 91 | * Improve debug output for diagnosing parser failures 92 | 93 | ### Fixes 94 | * Fix issue [#1](https://github.com/hansec/autocomplete-fortran/issues/1)- parsing error with "non-module" interface procedures 95 | 96 | ## 0.4.0 97 | 98 | ### Improvements 99 | * Add minimum word size before suggestions are provided 100 | * Add suggestion filtering for USE statements (restrict to modules and module members) 101 | * Disable suggestions in quoted strings 102 | * Reduce parsing frequency by preventing local buffer AST updates until edits on a line have completed 103 | * Refactor FORTRAN parser to improve accuracy/robustness 104 | 105 | ### Fixes 106 | * Fix parsing errors when adding a scope to the current buffer (automatically close scopes when parsing buffer) 107 | * Fix bug in parsing when generic "END" statement was followed by a comment 108 | * Fix bug in "GoTo Declaration" when declaration is in current buffer which is not included in the parsing directories 109 | 110 | ## 0.3.0 111 | 112 | ### Improvements 113 | * Add return type to function suggestions 114 | * Add POINTER/ALLOCATABLE information to variable suggestions 115 | * Indicate optional subroutine arguments ("arg=arg") 116 | * Provide argument list for procedure pointers with defined interfaces 117 | * Improve accuracy/robustness of scope identification for user-defined type fields 118 | * Improve speed by searching each imported module only once 119 | * Add keybinding for "GoTo Declaration" 120 | 121 | ### Fixes 122 | * Remove class "self" argument from type bound procedures 123 | * Fix issue with lower vs upper case in user-defined type fields 124 | * Restrict "GoTo Declaration" context menu to FORTRAN source files 125 | 126 | ## 0.2.0 127 | * Add initial support for fixed-format grammar 128 | * Add GoTo Declaration 129 | * Improve parser error handling/reporting 130 | 131 | ## 0.1.1 132 | * Update listed dependencies to include `language-fortran` 133 | 134 | ## 0.1.0 - First Release 135 | * Initial release 136 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Chris Hansen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # autocomplete-fortran package [![Package version](https://img.shields.io/apm/v/autocomplete-fortran.svg?style=flat-square)](https://atom.io/packages/autocomplete-fortran) [![Plugin installs](https://img.shields.io/apm/dm/autocomplete-fortran.svg?style=flat-square)](https://atom.io/packages/autocomplete-fortran) 2 | 3 | ## Warning: this package is now deprecated 4 | 5 | This package is no longer being updated. It has been replaced by the [ide-fortran](https://atom.io/packages/ide-fortran) package, which has improved functionality via common [IDE support in Atom](https://ide.atom.io/). 6 | 7 | ## Description 8 | 9 | This package provides autocomplete suggestions and "Go To Declaration" support for FORTRAN code using [autocomplete-plus](https://atom.io/packages/autocomplete-plus). 10 | 11 | *Note:* This package is experimental. If you find any bugs or if there are any missing features you would like please open an [issue](https://github.com/hansec/autocomplete-fortran/issues). 12 | 13 | ![Autocomplete in user-defined types](http://staff.washington.edu/hansec/ac_fortran_ex1.gif) 14 | 15 | ![Go To Declaration](http://staff.washington.edu/hansec/ac_fortran_ex2.gif) 16 | 17 | ## Requirements 18 | This package requires the following packages to be installed: 19 | * [autocomplete-plus](https://atom.io/packages/autocomplete-plus) 20 | * [language-fortran](https://atom.io/packages/language-fortran) 21 | 22 | Additionally, you must have [Python](https://www.python.org/) installed on your system. 23 | 24 | This package has been tested and *should* work on :apple: Mac OSX, :penguin: Linux and Windows 25 | 26 | ## Features 27 | * Provides suggestions across imported modules 28 | * Provides suggestions within user-defined types even when nested 29 | * Provides argument list for subroutine and function calls (optional arguments are indicated) 30 | * Indicates return type for function calls 31 | * "Go To Declaration" support for FORTRAN objects (including fields in user-defined types) 32 | * Support for generating and using external index files (completion for libraries outside project ex. MPI, BLAS/LAPACK, etc.) 33 | 34 | ## Usage 35 | Suggestions should be presented automatically while typing. At anytime you can force rebuilding of the index through the menu `Packages->Autocomplete FOTRAN->Rebuild index`. 36 | 37 | "Go To Declaration" is also supported for FORTRAN objects as the `FORTRAN-Goto Declaration` option in the context menu (right-click in editor). "Go To Declaration" can also be activated by the key binding `cmd-alt-g` on OS X and `ctrl-alt-g` on Linux/Windows. 38 | 39 | ### Notes 40 | * Initial setup of the index, including file parsing, is performed upon the first suggestion call. This may cause the first suggestion to take a moment to appear for very large projects (this is usually not noticeable). 41 | * After setup the index is dynamically updated as you modify files. However, if you edit a file outside of Atom (ex. switching branches in git,svn,etc.) changes will not be incorporated into the index until you edit the modified files in Atom or rebuild the index manually. 42 | * The grammar (fixed or free) is currently determined by file extension (`f,F,f77,F77,for,FOR,fpp,FPP` for fixed-form) and (`f90,F90,f95,F95,f03,F03,f08,F08` for free-form) 43 | 44 | ## Configuration 45 | 46 | ### Setup module search paths 47 | By default all files with the suffix `f,F,f77,F77,for,FOR,fpp,FPP` or `f90,F90,f95,F95,f03,F03,f08,F08` in the 48 | base atom project directory are parsed and used for generating suggestions. Specific folders containing FORTRAN 49 | source files can be set for a given project by placing a JSON file (example below) named `.ac_fortran` in the 50 | base directory. Folders to search are listed in the variable `mod_dirs` (relative to the project root) and excluded 51 | files can be specified using the variable `excl_paths`. Directories are not added recursively, so 52 | any nested sub directories must be explicitly listed. 53 | 54 | { 55 | "mod_dirs": ["subdir1", "subdir2"], 56 | "excl_paths": ["subdir1/file_to_skip.F90"] 57 | } 58 | 59 | ### External index files 60 | Additional autocompletion information can also be imported for use in the current project by specifying index files 61 | in the variable `ext_index` (relative to the project root) in your project's `.ac_fortran` file. Index files can be 62 | generated from a project through the menu through the menu `Packages->Autocomplete FOTRAN->Save external index file`. 63 | This action will save a file called `ac_fortran_index.json` in the root directory of the project containing all the 64 | information necessary to provide autocompletion for FORTRAN entities in the project. However, source file information 65 | will be stripped so "Go To Declaration" will not be available for externally imported entities. This file can then 66 | be used to make these entities available in a different project. Some useful index files for common libraries 67 | (ex. BLAS/LAPACK) are available at https://github.com/hansec/autocomplete-fortran-ext. 68 | 69 | { 70 | "ext_index": ["blas_index.json"] 71 | } 72 | 73 | ### Settings 74 | 75 | The FORTRAN parser is written in Python so a Python implementation is required to use this package. The path to Python may be set in package settings (required for Windows). 76 | 77 | Many additional settings, such as the selection keys (`TAB`/`ENTER`), are governed by the global settings for the [autocomplete-plus](https://atom.io/packages/autocomplete-plus) package. 78 | 79 | ## TODOs 80 | * Handle explicit PASS statement for type bound procedures 81 | * Allow fuzzy completion suggestions 82 | 83 | -------- 84 | 85 | If you *really* like [autocomplete-fortran](https://atom.io/packages/autocomplete-fortran) you can buy me a :coffee: or a :beer: to say thanks. 86 | -------------------------------------------------------------------------------- /keymaps/autocomplete-fortran.cson: -------------------------------------------------------------------------------- 1 | # Keybindings require three things to be fully defined: A selector that is 2 | # matched against the focused element, the keystroke and the command to 3 | # execute. 4 | # 5 | # Below is a basic keybinding which registers on all platforms by applying to 6 | # the root workspace element. 7 | 8 | # For more detailed documentation see 9 | # https://atom.io/docs/latest/behind-atom-keymaps-in-depth 10 | '.platform-darwin atom-text-editor[data-grammar~=fortran]:not(.mini)': 11 | 'alt-cmd-g': 'autocomplete-fortran:go-declaration' 12 | 13 | '.platform-linux atom-text-editor[data-grammar~=fortran]:not(.mini)': 14 | 'ctrl-alt-g': 'autocomplete-fortran:go-declaration' 15 | 16 | '.platform-win32 atom-text-editor[data-grammar~=fortran]:not(.mini)': 17 | 'ctrl-alt-g': 'autocomplete-fortran:go-declaration' 18 | -------------------------------------------------------------------------------- /lib/autocomplete-fortran.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable,File} = require 'atom' 2 | 3 | module.exports = 4 | config: 5 | pythonPath: 6 | type: 'string' 7 | default: '/usr/bin/python' 8 | order: 1 9 | title: 'Python Executable Path' 10 | description: "Optional path to python executable." 11 | minPrefix: 12 | type: 'integer' 13 | default: 2 14 | order: 2 15 | title: 'Minimum word length' 16 | description: "Only autocomplete when you have typed at least this many characters. Note: autocomplete is always active for user-defined type fields." 17 | preserveCase: 18 | type: 'boolean' 19 | default: true 20 | order: 3 21 | title: 'Preserve completion case' 22 | description: "Preserve case of suggestions from their defintion when inserting completions. Otherwise all suggestions will be lowercase." 23 | useSnippets: 24 | type: 'boolean' 25 | default: true 26 | order: 3 27 | title: 'Use argument snippets' 28 | description: "Use snippets for function/subroutine arguments. See: https://github.com/atom/snippets for more information." 29 | displayDepWarning: 30 | type: 'boolean' 31 | default: true 32 | order: 4 33 | title: 'Display deprecation warning' 34 | description: "This package has been replaced by the [ide-fortran](https://atom.io/packages/ide-fortran) package and is now deprecated. Use this option to disable warnings about this." 35 | provider: null 36 | 37 | activate: -> 38 | #console.log 'Activated AC-Fortran!' 39 | # Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable 40 | @subscriptions = new CompositeDisposable 41 | # Register command that rebuilds index 42 | @subscriptions.add atom.commands.add 'atom-workspace', 43 | 'autocomplete-fortran:rebuild': => @rebuild() 44 | @subscriptions.add atom.commands.add 'atom-workspace', 45 | 'autocomplete-fortran:saveIndex': => @saveIndex() 46 | @subscriptions.add atom.commands.add "atom-text-editor", 47 | 'autocomplete-fortran:go-declaration': (e)=> @goDeclaration atom.workspace.getActiveTextEditor(),e 48 | # Warn about deprecation 49 | if atom.config.get('autocomplete-fortran.displayDepWarning') 50 | atom.notifications.addWarning( 51 | "The `autocomplete-fortran` package is now deprecated.", 52 | { 53 | dismissable: true, 54 | buttons: [ 55 | { 56 | text: "Disable warning", 57 | onDidClick: () => 58 | atom.workspace.open("atom://config/packages/autocomplete-fortran") 59 | } 60 | ], 61 | description: 62 | "This package has been replaced by the [ide-fortran](https://atom.io/packages/ide-fortran) package. You may disable this warning in settings." 63 | } 64 | ) 65 | 66 | deactivate: -> 67 | @subscriptions.dispose() 68 | @provider = null 69 | 70 | provide: -> 71 | unless @provider? 72 | FortranProvider = require('./fortran-provider') 73 | @provider = new FortranProvider() 74 | @provider 75 | 76 | rebuild: ()-> 77 | #console.log "Rebuild triggered" 78 | if @provider? 79 | @provider.rebuildIndex() 80 | 81 | goDeclaration: (editor, e)-> 82 | editor.selectWordsContainingCursors() 83 | varWord = editor.getSelectedText() 84 | bufferRange = editor.getSelectedBufferRange() 85 | defPos = @provider.goToDef(varWord, editor, bufferRange.end) 86 | #console.log defPos 87 | if defPos? 88 | splitInfo = defPos.split(":") 89 | fileName = splitInfo[0] 90 | lineRef = splitInfo[1] 91 | f = new File fileName 92 | f.exists().then (result) -> 93 | atom.workspace.open fileName, {initialLine:lineRef-1, initialColumn:0} if result 94 | else 95 | atom.notifications?.addWarning("Could not find definition: '#{varWord}'", { 96 | dismissable: true 97 | }) 98 | 99 | saveIndex: () -> 100 | @provider.saveIndex() 101 | -------------------------------------------------------------------------------- /lib/fortran-provider.coffee: -------------------------------------------------------------------------------- 1 | {BufferedProcess, CompositeDisposable, File} = require 'atom' 2 | fs = require('fs') 3 | path = require('path') 4 | 5 | module.exports = 6 | class FortranProvider 7 | selector: '.source.fortran' 8 | disableForSelector: '.source.fortran .comment, .source.fortran .string.quoted' 9 | inclusionPriority: 1 10 | suggestionPriority: 2 11 | 12 | workspaceWatcher: undefined 13 | saveWatchers: undefined 14 | 15 | pythonPath: '' 16 | pythonValid: -1 17 | parserPath: '' 18 | minPrefix: 2 19 | preserveCase: true 20 | useSnippets: true 21 | firstRun: true 22 | indexReady: false 23 | globalUpToDate: true 24 | lastFile: '' 25 | lastRow: -1 26 | 27 | fileObjInd: { } 28 | fileObjLists: { } 29 | globalObjInd: [] 30 | projectObjList: { } 31 | exclPaths: [] 32 | modDirs: [] 33 | modFiles: [] 34 | fileIndexed: [] 35 | descList: [] 36 | 37 | constructor: () -> 38 | @pythonPath = atom.config.get('autocomplete-fortran.pythonPath') 39 | @parserPath = path.join(__dirname, "..", "python", "parse_fortran.py") 40 | @minPrefix = atom.config.get('autocomplete-fortran.minPrefix') 41 | @preserveCase = atom.config.get('autocomplete-fortran.preserveCase') 42 | @useSnippets = atom.config.get('autocomplete-fortran.useSnippets') 43 | @saveWatchers = new CompositeDisposable 44 | @workspaceWatcher = atom.workspace.observeTextEditors((editor) => @setupEditors(editor)) 45 | @checkPythonPath() 46 | 47 | destructor: () -> 48 | if @workspaceWatcher? 49 | @workspaceWatcher.dispose() 50 | if @saveWatchers? 51 | @saveWatchers.dispose() 52 | 53 | checkPythonPath: () -> 54 | command = @pythonPath 55 | stdOutput = "" 56 | errOutput = "" 57 | args = ["-V"] 58 | stdout = (output) => stdOutput = output 59 | stderr = (output) => errOutput = output 60 | exit = (code) => 61 | if @pythonValid == -1 62 | unless code == 0 63 | @pythonValid = 0 64 | if errOutput.indexOf('is not recognized as an internal or external') > -1 65 | @pythonValid = 0 66 | if @pythonValid == -1 67 | @pythonValid = 1 68 | else 69 | console.log '[ac-fortran] Python check failed' 70 | console.log '[ac-fortran]',errOutput 71 | bufferedProcess = new BufferedProcess({command, args, stdout, stderr, exit}) 72 | bufferedProcess.onWillThrowError ({error, handle}) => 73 | if error.code is 'ENOENT' and error.syscall.indexOf('spawn') is 0 74 | @pythonValid = 0 75 | console.log '[ac-fortran] Python check failed' 76 | console.log '[ac-fortran]',error 77 | handle() 78 | else 79 | throw error 80 | 81 | setupEditors: (editor) -> 82 | scopeDesc = editor.getRootScopeDescriptor().getScopesArray() 83 | if scopeDesc[0]?.indexOf('fortran') > -1 84 | @saveWatchers.add editor.onDidSave((event) => @fileUpdateSave(event)) 85 | 86 | fileUpdateSave: (event) -> 87 | if @pythonValid < 1 88 | if @pythonValid == 0 89 | @addError("Python path error", "Disabling FORTRAN autocompletion") 90 | @pythonValid = -2 91 | return 92 | fileRef = @modFiles.indexOf(event.path) 93 | if fileRef > -1 94 | @fileUpdate(event.path, true) 95 | 96 | rebuildIndex: () -> 97 | # Reset index 98 | @indexReady = false 99 | @globalUpToDate = true 100 | @lastFile = '' 101 | @lastRow = -1 102 | @modDirs = [] 103 | @modFiles = [] 104 | @fileIndexed = [] 105 | @fileObjInd = { } 106 | @fileObjLists = { } 107 | @globalObjInd = [] 108 | @projectObjList = { } 109 | @descList = [] 110 | # Build index 111 | @findModFiles() 112 | @filesUpdate(@modFiles) 113 | 114 | checkIndex: () -> 115 | if @indexReady 116 | return true 117 | for isIndexed in @fileIndexed 118 | unless isIndexed 119 | return false 120 | @indexReady = true 121 | return true 122 | 123 | addInfo: (info, detail=null) -> 124 | if detail? 125 | atom.notifications?.addInfo("ac-fortran: #{info}", {detail: detail}) 126 | else 127 | atom.notifications?.addInfo("ac-fortran: #{info}") 128 | 129 | addError: (info, detail=null) -> 130 | if detail? 131 | atom.notifications?.addError("ac-fortran: #{info}", {detail: detail}) 132 | else 133 | atom.notifications?.addError("ac-fortran: #{info}") 134 | 135 | notifyIndexPending: (operation) -> 136 | atom.notifications?.addWarning("Could not complete operation: #{operation}", { 137 | detail: 'Indexing pending', 138 | dismissable: true 139 | }) 140 | 141 | findModFiles: ()-> 142 | freeRegex = /[a-z0-9_]*\.F(90|95|03|08)$/i # f90,F90,f95,F95,f03,F03,f08,F08 143 | fixedRegex = /[a-z0-9_]*\.F(77|OR|PP)?$/i # f,F,f77,F77,for,FOR,fpp,FPP 144 | projectDirs = atom.project.getPaths() 145 | @modDirs = projectDirs 146 | @exclPaths = [] 147 | extPaths = [] 148 | for projDir in projectDirs 149 | settingPath = path.join(projDir, '.ac_fortran') 150 | try 151 | fs.accessSync(settingPath, fs.R_OK) 152 | fs.openSync(settingPath, 'r+') 153 | result = fs.readFileSync(settingPath) 154 | try 155 | configOptions = JSON.parse(result) 156 | catch 157 | @addError("Error reading project settings", "path #{settingPath}") 158 | continue 159 | if 'excl_paths' of configOptions 160 | for exclPath in configOptions['excl_paths'] 161 | @exclPaths.push(path.join(projDir, exclPath)) 162 | if 'mod_dirs' of configOptions 163 | @modDirs = [] 164 | for modDir in configOptions['mod_dirs'] 165 | @modDirs.push(path.join(projDir, modDir)) 166 | if 'ext_index' of configOptions 167 | for relPath in configOptions['ext_index'] 168 | indexPath = path.join(projDir, relPath) 169 | try 170 | fs.accessSync(indexPath, fs.R_OK) 171 | fs.openSync(indexPath, 'r+') 172 | result = fs.readFileSync(indexPath) 173 | extIndex = JSON.parse(result) 174 | objListing = extIndex['obj'] 175 | descListing = extIndex['descs'] 176 | for key of objListing 177 | @projectObjList[key] = objListing[key] 178 | obj = @projectObjList[key] 179 | descInd = obj['desc'] 180 | descStr = descListing[descInd] 181 | if descStr? 182 | descIndex = @descList.indexOf(descStr) 183 | if descIndex == -1 184 | @descList.push(descStr) 185 | obj['desc'] = @descList.length-1 186 | else 187 | obj['desc'] = descIndex 188 | extPaths.push("#{relPath}") 189 | catch 190 | @addError("Cannot read external index file", "path #{relPath}") 191 | if extPaths.length > 0 192 | @addInfo("Added external index files", extPaths.join('\n')) 193 | for modDir in @modDirs 194 | try 195 | files = fs.readdirSync(modDir) 196 | catch 197 | atom.notifications?.addWarning("Warning: During indexing specified module directory cannot be read", { 198 | detail: "Directory '#{modDir}' will be skipped", 199 | dismissable: true 200 | }) 201 | continue 202 | for file in files 203 | if file.match(freeRegex) or file.match(fixedRegex) 204 | filePath = path.join(modDir, file) 205 | if @exclPaths.indexOf(filePath) == -1 206 | @modFiles.push(filePath) 207 | @fileIndexed.push(false) 208 | 209 | filesUpdate: (filePaths, closeScopes=false)-> 210 | fixedRegex = /[a-z0-9_]*\.F(77|OR|PP)?$/i # f,F,f77,F77,for,FOR,fpp,FPP 211 | command = @pythonPath 212 | # 213 | fixedBatch = [] 214 | freeBatch = [] 215 | for filePath in filePaths 216 | if filePath.match(fixedRegex) 217 | fixedBatch.push(filePath) 218 | else 219 | freeBatch.push(filePath) 220 | # 221 | if fixedBatch.length > 0 222 | fixedFilePaths = fixedBatch.join(',') 223 | new Promise (resolve) => 224 | allOutput = [] 225 | args = [@parserPath, "--files=#{fixedFilePaths}", "--fixed"] 226 | if closeScopes 227 | args.push("--close_scopes") 228 | stdout = (output) => allOutput.push(output) 229 | stderr = (output) => console.log output 230 | exit = (code) => resolve(@handleParserResults(allOutput.join(''), code, fixedBatch)) 231 | fixedBufferedProcess = new BufferedProcess({command, args, stdout, stderr, exit}) 232 | # 233 | if freeBatch.length > 0 234 | freeFilePaths = freeBatch.join(',') 235 | new Promise (resolve) => 236 | allOutput = [] 237 | args = [@parserPath, "--files=#{freeFilePaths}"] 238 | if closeScopes 239 | args.push("--close_scopes") 240 | stdout = (output) => allOutput.push(output) 241 | stderr = (output) => console.log output 242 | exit = (code) => resolve(@handleParserResults(allOutput.join(''), code, freeBatch)) 243 | freeBufferedProcess = new BufferedProcess({command, args, stdout, stderr, exit}) 244 | 245 | fileUpdate: (filePath, closeScopes=false)-> 246 | fixedRegex = /[a-z0-9_]*\.F(77|OR|PP)?$/i # f,F,f77,F77,for,FOR,fpp,FPP 247 | command = @pythonPath 248 | args = [@parserPath,"--files=#{filePath}"] 249 | if filePath.match(fixedRegex) 250 | args.push("--fixed") 251 | if closeScopes 252 | args.push("--close_scopes") 253 | # 254 | new Promise (resolve) => 255 | allOutput = [] 256 | stdout = (output) => allOutput.push(output) 257 | stderr = (output) => console.log output 258 | exit = (code) => resolve(@handleParserResult(allOutput.join('\n'), code, filePath)) 259 | bufferedProcess = new BufferedProcess({command, args, stdout, stderr, exit}) 260 | 261 | localUpdate: (editor, row)-> 262 | fixedRegex = /[a-z0-9_]*\.F(77|OR|PP)?$/i # f,F,f77,F77,for,FOR,fpp,FPP 263 | filePath = editor.getPath() 264 | command = @pythonPath 265 | args = [@parserPath,"-s"] 266 | if filePath.match(fixedRegex) 267 | args.push("--fixed") 268 | # 269 | new Promise (resolve) => 270 | allOutput = [] 271 | stdout = (output) => allOutput.push(output) 272 | stderr = (output) => console.log output 273 | exit = (code) => resolve(@handleParserResult(allOutput.join('\n'), code, filePath)) 274 | bufferedProcess = new BufferedProcess({command, args, stdout, stderr, exit}) 275 | bufferedProcess.process.stdin.setEncoding = 'utf-8'; 276 | bufferedProcess.process.stdin.write(editor.getText()) 277 | bufferedProcess.process.stdin.end() 278 | 279 | handleParserResults: (results,returnCode,filePaths) -> 280 | if returnCode is not 0 281 | return 282 | resultsSplit = results.split('\n') 283 | nResults = resultsSplit.length - 1 284 | nFiles = filePaths.length 285 | if nResults != nFiles 286 | console.log 'Error parsing files: # of files and results does not match', nResults, nFiles 287 | return 288 | for i in [0..nFiles-1] 289 | @handleParserResult(resultsSplit[i],returnCode,filePaths[i]) 290 | 291 | handleParserResult: (result,returnCode,filePath) -> 292 | if returnCode is not 0 293 | return 294 | try 295 | fileAST = JSON.parse(result) 296 | catch 297 | console.log 'Error parsing file:', filePath 298 | atom.notifications?.addError("Error parsing file '#{filePath}'", { 299 | detail: 'Script failed', 300 | dismissable: true 301 | }) 302 | return 303 | # 304 | if 'error' of fileAST 305 | console.log 'Error parsing file:', filePath 306 | atom.notifications?.addError("Error parsing file '#{filePath}'", { 307 | detail: fileAST['error'], 308 | dismissable: true 309 | }) 310 | return 311 | # 312 | fileRef = @modFiles.indexOf(filePath) 313 | if fileRef == -1 314 | @modFiles.push(filePath) 315 | fileRef = @modFiles.indexOf(filePath) 316 | oldObjList = @fileObjLists[filePath] 317 | @fileObjLists[filePath] = [] 318 | for key of fileAST['objs'] 319 | @fileObjLists[filePath].push(key) 320 | if key of @projectObjList 321 | @resetInherit(@projectObjList[key]) 322 | @projectObjList[key] = fileAST['objs'][key] 323 | @projectObjList[key]['file'] = fileRef 324 | if 'desc' of @projectObjList[key] 325 | descIndex = @descList.indexOf(@projectObjList[key]['desc']) 326 | if descIndex == -1 327 | @descList.push(@projectObjList[key]['desc']) 328 | @projectObjList[key]['desc'] = @descList.length-1 329 | else 330 | @projectObjList[key]['desc'] = descIndex 331 | # Remove old objects 332 | if oldObjList? 333 | for key in oldObjList 334 | unless key of fileAST['objs'] 335 | delete @projectObjList[key] 336 | @fileObjInd[filePath] = fileAST['scopes'] 337 | @fileIndexed[fileRef] = true 338 | @globalUpToDate = false 339 | 340 | updateGlobalIndex: () -> 341 | if @globalUpToDate 342 | return 343 | @globalObjInd = [] 344 | for key of @projectObjList 345 | if not key.match(/::/) 346 | @globalObjInd.push(key) 347 | 348 | getSuggestions: ({editor, bufferPosition, prefix, activatedManually}) -> 349 | if @pythonValid < 1 350 | if @pythonValid == 0 351 | @addError("Python path error", "Disabling FORTRAN autocompletion") 352 | @pythonValid = -2 353 | return 354 | unless @exclPaths.indexOf(editor.getPath()) == -1 355 | return [] 356 | # Build index on first run 357 | if @firstRun 358 | @rebuildIndex() 359 | @firstRun = false 360 | return new Promise (resolve) => 361 | # Check if update requred 362 | parseBuffer = false 363 | if @lastFile != editor.getPath() 364 | parseBuffer = true 365 | @lastFile = editor.getPath() 366 | if @lastRow != bufferPosition.row 367 | parseBuffer = true 368 | @lastRow = bufferPosition.row 369 | # Get suggestions 370 | if parseBuffer 371 | @localUpdate(editor, bufferPosition.row).then () => 372 | resolve(@filterSuggestions(prefix, editor, bufferPosition, activatedManually)) 373 | else 374 | resolve(@filterSuggestions(prefix, editor, bufferPosition, activatedManually)) 375 | 376 | filterSuggestions: (prefix, editor, bufferPosition, activatedManually) -> 377 | completions = [] 378 | suggestions = [] 379 | @updateGlobalIndex() 380 | if prefix 381 | prefixLower = prefix.toLowerCase() 382 | fullLine = @getFullLine(editor, bufferPosition) 383 | lineContext = @getLineContext(fullLine) 384 | if lineContext == 2 385 | return completions 386 | if lineContext == 1 387 | suggestions = @getUseSuggestion(fullLine, prefixLower) 388 | return @buildCompletionList(suggestions, lineContext) 389 | lineScopes = @getLineScopes(editor, bufferPosition) 390 | cursorScope = @getClassScope(fullLine, lineScopes) 391 | if cursorScope? 392 | suggestions = @addChildren(cursorScope, suggestions, prefixLower, []) 393 | return @buildCompletionList(suggestions, lineContext) 394 | if prefix.length < @minPrefix and not activatedManually 395 | return completions 396 | for key in @globalObjInd when (@projectObjList[key]['name'].toLowerCase().startsWith(prefixLower)) 397 | if @projectObjList[key]['type'] == 1 398 | continue 399 | suggestions.push(key) 400 | # 401 | usedMod = { } 402 | for lineScope in lineScopes 403 | suggestions = @addChildren(lineScope, suggestions, prefixLower, []) 404 | usedMod = @getUseSearches(lineScope, usedMod, []) 405 | for useMod of usedMod 406 | suggestions = @addPublicChildren(useMod, suggestions, prefixLower, usedMod[useMod]) 407 | completions = @buildCompletionList(suggestions, lineContext) 408 | else 409 | line = editor.getTextInRange([[bufferPosition.row, 0], bufferPosition]) 410 | unless line.endsWith('%') 411 | return completions 412 | fullLine = @getFullLine(editor, bufferPosition) 413 | lineContext = @getLineContext(fullLine) 414 | lineScopes = @getLineScopes(editor, bufferPosition) 415 | cursorScope = @getClassScope(fullLine, lineScopes) 416 | if cursorScope? 417 | suggestions = @addChildren(cursorScope, suggestions, prefixLower, []) 418 | return @buildCompletionList(suggestions,lineContext) 419 | return completions 420 | 421 | saveIndex: () -> 422 | # Build index on first run 423 | if @firstRun 424 | @rebuildIndex() 425 | @firstRun = false 426 | unless @checkIndex() 427 | @notifyIndexPending('Save Index') 428 | return 429 | removalList = [] 430 | for key of @projectObjList 431 | obj = @projectObjList[key] 432 | type = obj['type'] 433 | if type == 2 or type == 3 434 | memList = obj['mem'] 435 | if memList? 436 | for member in memList 437 | removalList.push(key+'::'+member.toLowerCase()) 438 | delete obj['mem'] 439 | for key in removalList 440 | delete @projectObjList[key] 441 | newDescList = [] 442 | newDescs = [] 443 | for key of @projectObjList 444 | obj = @projectObjList[key] 445 | if obj['type'] == 7 446 | @resolveInterface(key) 447 | @resolveIherited(key) 448 | delete obj['fdef'] 449 | delete obj['file'] 450 | delete obj['fbound'] 451 | desInd = obj['desc'] 452 | descIndex = newDescList.indexOf(desInd) 453 | if descIndex == -1 454 | newDescList.push(desInd) 455 | newDescs.push(@descList[desInd]) 456 | obj['desc'] = newDescList.length-1 457 | else 458 | obj['desc'] = descIndex 459 | outObj = {'obj': @projectObjList, 'descs': newDescs} 460 | projectDirs = atom.project.getPaths() 461 | outputPath = path.join(projectDirs[0], 'ac_fortran_index.json') 462 | fd = fs.openSync(outputPath, 'w+') 463 | fs.writeSync(fd, JSON.stringify(outObj)) 464 | fs.closeSync(fd) 465 | @rebuildIndex() 466 | 467 | goToDef: (word, editor, bufferPosition) -> 468 | # Build index on first run 469 | if @firstRun 470 | @rebuildIndex() 471 | @firstRun = false 472 | @localUpdate(editor, bufferPosition.row) 473 | unless @checkIndex() 474 | @notifyIndexPending('Go To Definition') 475 | return 476 | @updateGlobalIndex() 477 | wordLower = word.toLowerCase() 478 | lineScopes = @getLineScopes(editor, bufferPosition) 479 | # Look up class tree 480 | fullLine = @getFullLine(editor, bufferPosition) 481 | cursorScope = @getClassScope(fullLine, lineScopes) 482 | if cursorScope? 483 | @resolveIherited(cursorScope) 484 | containingScope = @findInScope(cursorScope, wordLower) 485 | if containingScope? 486 | FQN = containingScope+"::"+wordLower 487 | return @getDefLoc(@projectObjList[FQN]) 488 | # Look in global context 489 | if @globalObjInd.indexOf(wordLower) != -1 490 | return @getDefLoc(@projectObjList[wordLower]) 491 | # Look in local scopes 492 | for lineScope in lineScopes 493 | containingScope = @findInScope(lineScope, wordLower) 494 | if containingScope? 495 | FQN = containingScope+"::"+wordLower 496 | return @getDefLoc(@projectObjList[FQN]) 497 | return null 498 | 499 | getDefLoc: (varObj) -> 500 | fileRef = varObj['file'] 501 | lineRef = null 502 | if 'fdef' of varObj 503 | lineRef = varObj['fdef'] 504 | if 'fbound' of varObj 505 | lineRef = varObj['fbound'][0] 506 | if lineRef? 507 | return @modFiles[fileRef]+":"+lineRef.toString() 508 | return null 509 | 510 | getUseSuggestion: (line, prefixLower) -> 511 | useRegex = /^[ \t]*use[ \t]+/i 512 | wordRegex = /[a-z0-9_]+/gi 513 | suggestions = [] 514 | if line.match(useRegex)? 515 | unless prefixLower.match(wordRegex)? 516 | prefixLower = "" 517 | matches = line.match(wordRegex) 518 | if matches.length == 2 519 | if prefixLower? 520 | for key in @globalObjInd when (@projectObjList[key]['name'].toLowerCase().startsWith(prefixLower)) 521 | if @projectObjList[key]['type'] != 1 522 | continue 523 | suggestions.push(key) 524 | else 525 | for key in @globalObjInd 526 | suggestions.push(key) 527 | else if matches.length > 2 528 | modName = matches[1] 529 | suggestions = @addPublicChildren(modName, suggestions, prefixLower, []) 530 | return suggestions # Unknown enable everything!!!! 531 | 532 | getFullLine: (editor, bufferPosition) -> 533 | fixedRegex = /[a-z0-9_]*\.F(77|OR|PP)?$/i # f,F,f77,F77,for,FOR,fpp,FPP 534 | fixedCommRegex = /^ [\S]/i 535 | freeCommRegex = /&[ \t]*$/i 536 | line = editor.getTextInRange([[bufferPosition.row, 0], bufferPosition]) 537 | # 538 | fixedForm = false 539 | if editor.getPath().match(fixedRegex) 540 | fixedForm = true 541 | pRow = bufferPosition.row - 1 542 | while pRow >= 0 543 | pLine = editor.lineTextForBufferRow(pRow) 544 | pLine = pLine.split('!')[0] 545 | if fixedForm 546 | unless line.match(fixedCommRegex) 547 | break 548 | else 549 | unless pLine.match(freeCommRegex) 550 | break 551 | line = pLine.split('&')[0] + line 552 | pRow = pRow - 1 553 | return line 554 | 555 | getLineContext: (line) -> 556 | useRegex = /^[ \t]*USE[ \t]/i 557 | subDefRegex = /^[ \t]*(PURE|ELEMENTAL|RECURSIVE)*[ \t]*(MODULE|PROGRAM|SUBROUTINE|FUNCTION)[ \t]/i 558 | typeDefRegex = /^[ \t]*(CLASS|TYPE)[ \t]*(IS)?[ \t]*\(/i 559 | callRegex = /^[ \t]*CALL[ \t]+[a-z0-9_%]*$/i 560 | deallocRegex = /^[ \t]*DEALLOCATE[ \t]*\(/i 561 | nullifyRegex = /^[ \t]*NULLIFY[ \t]*\(/i 562 | if line.match(callRegex)? 563 | return 4 564 | if line.match(deallocRegex)? 565 | return 5 566 | if line.match(nullifyRegex)? 567 | return 6 568 | if line.match(useRegex)? 569 | return 1 570 | if line.match(useRegex)? 571 | return 2 572 | if line.match(typeDefRegex)? 573 | return 3 574 | return 0 575 | 576 | getLineScopes: (editor, bufferPosition) -> 577 | filePath = editor.getPath() 578 | scopes = [] 579 | unless @fileObjInd[filePath]? 580 | return [] 581 | for key in @fileObjInd[filePath] # Look in currently active file for enclosing scopes 582 | if key of @projectObjList 583 | if bufferPosition.row+1 < @projectObjList[key]['fbound'][0] 584 | continue 585 | if bufferPosition.row+1 > @projectObjList[key]['fbound'][1] 586 | continue 587 | scopes.push(key) 588 | return scopes 589 | 590 | findInScope: (scope, name) -> 591 | FQN = scope + '::' + name 592 | if FQN of @projectObjList 593 | return scope 594 | scopeObj = @projectObjList[scope] 595 | unless scopeObj? 596 | return null 597 | # Check inherited 598 | if 'in_mem' of scopeObj 599 | for childKey in scopeObj['in_mem'] 600 | childScopes = childKey.split('::') 601 | childName = childScopes.pop() 602 | if childName == name 603 | return childScopes.join('::') 604 | # Search in use 605 | result = null 606 | usedMod = @getUseSearches(scope, { }, []) 607 | for useMod of usedMod 608 | if usedMod[useMod].length > 0 609 | if usedMod[useMod].indexOf(name) == -1 610 | continue 611 | result = @findInScope(useMod, name) 612 | if result? 613 | return result 614 | # Search parent 615 | if not result? 616 | endOfScope = scope.lastIndexOf('::') 617 | if endOfScope >=0 618 | newScope = scope.substring(0,endOfScope) 619 | result = @findInScope(newScope, name) 620 | return result 621 | 622 | getVarType: (varKey) -> 623 | varDesc = @descList[@projectObjList[varKey]['desc']] 624 | typeDef = varDesc.toLowerCase() 625 | i1 = typeDef.indexOf('(') 626 | i2 = typeDef.indexOf(')') 627 | return typeDef.substring(i1+1,i2) 628 | 629 | getClassScope: (line, currScopes) -> 630 | typeDerefCheck = /%/i 631 | objBreakReg = /[\/\-(.,+*<>=$:]/ig 632 | parenRepReg = /\((.+)\)/ig 633 | # 634 | unless line.match(typeDerefCheck)? 635 | return null 636 | parenCount = 0 637 | lineCopy = line 638 | for i in [0..lineCopy.length-1] 639 | currChar = lineCopy[lineCopy.length-i-1] 640 | if parenCount == 0 and currChar.match(objBreakReg) 641 | line = lineCopy.substring(lineCopy.length-i) 642 | break 643 | if currChar == '(' 644 | parenCount -= 1 645 | if currChar == ')' 646 | parenCount += 1 647 | searchScope = null 648 | if line.match(typeDerefCheck)? 649 | lineNoParen1 = line.replace(parenRepReg,'$') 650 | lineNoParen = lineNoParen1.replace(/\$%/i,'%') 651 | lineCommBreak = lineNoParen.replace(objBreakReg, ' ') 652 | lastSpace = lineCommBreak.lastIndexOf(' ') 653 | if lastSpace >=0 654 | lineNoParen = lineCommBreak.substring(lastSpace+1) 655 | splitLine = lineNoParen.split('%') 656 | prefixVar = splitLine.pop() 657 | for varName in splitLine 658 | varNameLower = varName.toLowerCase() 659 | if searchScope? 660 | @resolveIherited(searchScope) 661 | containingScope = @findInScope(searchScope, varNameLower) 662 | if containingScope? 663 | varKey = containingScope + "::" + varNameLower 664 | if @projectObjList[varKey]['type'] == 6 665 | varDefName = @getVarType(varKey) 666 | iLast = containingScope.lastIndexOf("::") 667 | typeScope = containingScope 668 | if iLast > -1 669 | typeScope = containingScope.substring(0,iLast) 670 | containingScope = @findInScope(typeScope, varDefName) 671 | searchScope = containingScope + '::' + varDefName 672 | else 673 | return null 674 | else 675 | for currScope in currScopes 676 | @resolveIherited(currScope) 677 | containingScope = @findInScope(currScope, varNameLower) 678 | if containingScope? 679 | varKey = containingScope + "::" + varNameLower 680 | if @projectObjList[varKey]['type'] == 6 681 | varDefName = @getVarType(varKey) 682 | iLast = containingScope.lastIndexOf("::") 683 | typeScope = containingScope 684 | if iLast > -1 685 | typeScope = containingScope.substring(0,iLast) 686 | containingScope = @findInScope(typeScope, varDefName) 687 | searchScope = containingScope + '::' + varDefName 688 | break 689 | return searchScope # Unknown enable everything!!!! 690 | 691 | addChildren: (scope, completions, prefix, onlyList) -> 692 | scopeObj = @projectObjList[scope] 693 | unless scopeObj? 694 | return completions 695 | children = scopeObj['mem'] 696 | unless children? 697 | return 698 | for child in children 699 | childLower = child.toLowerCase() 700 | if prefix? 701 | unless childLower.startsWith(prefix) 702 | continue 703 | if onlyList.length > 0 704 | if onlyList.indexOf(childLower) == -1 705 | continue 706 | childKey = scope+'::'+childLower 707 | if childKey of @projectObjList 708 | completions.push(childKey) 709 | # Add inherited 710 | @resolveIherited(scope) 711 | if 'in_mem' of scopeObj 712 | for childKey in scopeObj['in_mem'] 713 | completions.push(childKey) 714 | return completions 715 | 716 | getUseSearches: (scope, modDict, onlyList) -> 717 | # Process USE STMT (only if no onlyList) 718 | useList = @projectObjList[scope]['use'] 719 | if useList? 720 | for useMod in useList 721 | if useMod[0] of @projectObjList 722 | mergedOnly = @getOnlyOverlap(onlyList, useMod[1]) 723 | unless mergedOnly? 724 | continue 725 | if useMod[0] of modDict 726 | if modDict[useMod[0]].length > 0 727 | if mergedOnly.length == 0 728 | modDict[useMod[0]] = [] 729 | else 730 | for only in mergedOnly 731 | if modDict[useMod[0]].indexOf(only) == -1 732 | modDict[useMod[0]].push(only) 733 | else 734 | modDict[useMod[0]] = mergedOnly 735 | modDict = @getUseSearches(useMod[0], modDict, mergedOnly) 736 | return modDict 737 | 738 | getOnlyOverlap: (currList, newList) -> 739 | if currList.length == 0 740 | return newList 741 | if newList.length == 0 742 | return currList 743 | mergeList = [] 744 | hasOverlap = false 745 | for elem in newList 746 | unless currList.indexOf(elem) == -1 747 | mergeList.push(elem) 748 | hasOverlap = true 749 | if hasOverlap 750 | return mergeList 751 | else 752 | return null 753 | 754 | addPublicChildren: (scope, completions, prefix, onlyList) -> 755 | scopeObj = @projectObjList[scope] 756 | unless scopeObj? 757 | return completions 758 | children = scopeObj['mem'] 759 | unless children? 760 | return 761 | currVis = 1 762 | if 'vis' of scopeObj 763 | currVis = parseInt(scopeObj['vis']) 764 | for child in children 765 | childLower = child.toLowerCase() 766 | if prefix? 767 | unless childLower.startsWith(prefix) 768 | continue 769 | if onlyList.length > 0 770 | if onlyList.indexOf(childLower) == -1 771 | continue 772 | childKey = scope+'::'+childLower 773 | childObj = @projectObjList[childKey] 774 | if childObj? 775 | if 'vis' of childObj 776 | if parseInt(childObj['vis']) + currVis < 0 777 | continue 778 | else 779 | if currVis < 0 780 | continue 781 | completions.push(childKey) 782 | # Add inherited 783 | @resolveIherited(scope) 784 | if 'in_mem' of scopeObj 785 | for childKey in scopeObj['in_mem'] 786 | completions.push(childKey) 787 | return completions 788 | 789 | resolveInterface: (intObjKey) -> 790 | intObj = @projectObjList[intObjKey] 791 | if 'res_mem' of intObj 792 | return 793 | enclosingScope = @getEnclosingScope(intObjKey) 794 | unless enclosingScope? 795 | return 796 | resolvedChildren = [] 797 | children = intObj['mem'] 798 | for copyKey in children 799 | resolvedScope = @findInScope(enclosingScope, copyKey) 800 | if resolvedScope? 801 | resolvedChildren.push(resolvedScope+"::"+copyKey) 802 | intObj['res_mem'] = resolvedChildren 803 | 804 | resolveLink: (objKey) -> 805 | varObj = @projectObjList[objKey] 806 | linkKey = varObj['link'] 807 | unless linkKey? 808 | return 809 | if 'res_link' of varObj 810 | return 811 | enclosingScope = @getEnclosingScope(objKey) 812 | unless enclosingScope? 813 | return 814 | resolvedScope = @findInScope(enclosingScope, linkKey) 815 | if resolvedScope? 816 | varObj['res_link'] = resolvedScope+"::"+linkKey 817 | 818 | addChild: (scopeKey, childKey) -> 819 | if 'chld' of @projectObjList[scopeKey] 820 | if @projectObjList[scopeKey]['chld'].indexOf(childKey) == -1 821 | @projectObjList[scopeKey]['chld'].push(childKey) 822 | else 823 | @projectObjList[scopeKey]['chld'] = [childKey] 824 | 825 | resetInherit: (classObj) -> 826 | if 'in_mem' of classObj 827 | delete classObj['in_mem'] 828 | if 'res_parent' of classObj 829 | delete classObj['res_parent'] 830 | if 'chld' of classObj 831 | for childKey of classObj['chld'] 832 | childObj = @projectObjList[childKey] 833 | if childObj? 834 | @resetInherit(childObj) 835 | 836 | resolveIherited: (scope) -> 837 | classObj = @projectObjList[scope] 838 | if 'in_mem' of classObj 839 | return 840 | unless 'parent' of classObj 841 | return 842 | unless 'res_parent' of classObj 843 | parentName = classObj['parent'] 844 | resolvedScope = @findInScope(scope, parentName) 845 | if resolvedScope? 846 | classObj['res_parent'] = resolvedScope+"::"+parentName 847 | else 848 | return 849 | # Load from parent class 850 | parentKey = classObj['res_parent'] 851 | parentObj = @projectObjList[parentKey] 852 | if parentObj? 853 | @addChild(parentKey, scope) 854 | @resolveIherited(parentKey) 855 | # 856 | classObj['in_mem'] = [] 857 | if 'mem' of classObj 858 | classChildren = classObj['mem'] 859 | else 860 | classChildren = [] 861 | if 'mem' of parentObj 862 | for childKey in parentObj['mem'] 863 | if classChildren.indexOf(childKey) == -1 864 | classObj['in_mem'].push(parentKey+'::'+childKey) 865 | if 'in_mem' of parentObj 866 | for childKey in parentObj['in_mem'] 867 | childName = childKey.split('::').pop() 868 | if classChildren.indexOf(childName) == -1 869 | classObj['in_mem'].push(childKey) 870 | return 871 | 872 | getEnclosingScope: (objKey) -> 873 | finalSep = objKey.lastIndexOf('::') 874 | if finalSep == -1 875 | return null 876 | return objKey.substring(0,finalSep) 877 | 878 | buildCompletionList: (suggestions, contextFilter=0) -> 879 | subTestRegex = /^(TYP|CLA|PRO)/i 880 | typRegex = /^(TYP|CLA)/i 881 | completions = [] 882 | for suggestion in suggestions 883 | compObj = @projectObjList[suggestion] 884 | if contextFilter == 3 and compObj['type'] != 4 885 | continue 886 | if contextFilter == 4 887 | if compObj['type'] == 3 or compObj['type'] == 4 888 | continue 889 | if compObj['type'] == 6 890 | unless @descList[compObj['desc']].match(subTestRegex)? 891 | continue 892 | if contextFilter == 5 or contextFilter == 6 893 | if compObj['type'] == 6 894 | modList = compObj['mods'] 895 | isPoint = false 896 | isAlloc = false 897 | if modList? 898 | isPoint = (modList.indexOf(1) > -1) 899 | if contextFilter == 5 900 | isAlloc = (modList.indexOf(2) > -1) 901 | isType = @descList[compObj['desc']].match(typRegex)? 902 | unless (isPoint or isAlloc or isType) 903 | continue 904 | else 905 | continue 906 | if compObj['type'] == 7 907 | @resolveInterface(suggestion) 908 | repName = compObj['name'] 909 | for copyKey in compObj['res_mem'] 910 | completions.push(@buildCompletion(@projectObjList[copyKey], repName)) 911 | else 912 | if 'link' of compObj 913 | @resolveLink(suggestion) 914 | repName = compObj['name'] 915 | copyKey = compObj['res_link'] 916 | if copyKey? 917 | doPass = @testPass(compObj) 918 | completions.push(@buildCompletion(@projectObjList[copyKey], repName, doPass)) 919 | else 920 | completions.push(@buildCompletion(compObj)) 921 | else 922 | completions.push(@buildCompletion(compObj)) 923 | # 924 | if contextFilter == 1 925 | for completion in completions 926 | if 'snippet' of completion 927 | completion['snippet'] = completion['snippet'].split('(')[0] 928 | return completions 929 | 930 | buildCompletion: (suggestion, repName=null, stripArg=false) -> 931 | name = suggestion['name'] 932 | if repName? 933 | name = repName 934 | mods = @getModifiers(suggestion) 935 | compObj = {} 936 | compObj.type = @mapType(suggestion['type']) 937 | compObj.leftLabel = @descList[suggestion['desc']] 938 | unless @preserveCase 939 | name = name.toLowerCase() 940 | if 'args' of suggestion 941 | argStr = suggestion['args'] 942 | if @useSnippets 943 | argList = argStr.split(',') 944 | argListFinal = [] 945 | i = 0 946 | for arg in argList 947 | i += 1 948 | if stripArg and i == 1 949 | continue 950 | i1 = arg.indexOf("=") 951 | if i1 == -1 952 | argListFinal.push("${#{i}:#{arg}}") 953 | else 954 | argName = arg.substring(0,i1) 955 | argListFinal.push("#{argName}=${#{i}:#{argName}}") 956 | argStr = argListFinal.join(',') 957 | else 958 | if stripArg 959 | i1 = argStr.indexOf(',') 960 | if i1 > -1 961 | argStr = argStr.substring(i1+1).trim() 962 | else 963 | argStr = '' 964 | unless @preserveCase 965 | argStr = argStr.toLowerCase() 966 | compObj.snippet = name + "(" + argStr + ")" 967 | else 968 | compObj.text = name 969 | if mods != '' 970 | compObj.description = mods 971 | return compObj 972 | #rightLabel: 'My Provider' 973 | 974 | mapType: (typeInd) -> 975 | switch typeInd 976 | when 1 then return 'module' 977 | when 2 then return 'method' 978 | when 3 then return 'function' 979 | when 4 then return 'class' 980 | when 5 then return 'interface' 981 | when 6 then return 'variable' 982 | return 'unknown' 983 | 984 | getModifiers: (suggestion) -> 985 | modList = [] 986 | if 'mods' of suggestion 987 | for mod in suggestion['mods'] 988 | if mod > 20 989 | ndims = mod-20 990 | dimStr = "DIMENSION(:" 991 | if ndims > 1 992 | for i in [2..ndims] 993 | dimStr += ",:" 994 | dimStr += ")" 995 | modList.push(dimStr) 996 | continue 997 | switch mod 998 | when 1 then modList.push("POINTER") 999 | when 2 then modList.push("ALLOCATABLE") 1000 | return modList.join(', ') 1001 | 1002 | testPass: (obj) -> 1003 | if 'mods' of obj 1004 | ind = obj['mods'].indexOf(6) 1005 | if ind != -1 1006 | return false 1007 | return true 1008 | -------------------------------------------------------------------------------- /menus/autocomplete-fortran.cson: -------------------------------------------------------------------------------- 1 | 'context-menu': 2 | "atom-text-editor[data-grammar~=fortran]:not(.mini)":[ 3 | {label:'FORTRAN-Goto Declaration', command:'autocomplete-fortran:go-declaration'} 4 | ] 5 | 6 | 'menu': [ 7 | { 8 | label: 'Packages' 9 | submenu: [ 10 | label: 'Autocomplete FORTRAN' 11 | submenu: [ 12 | {label:'Rebuild index', command:'autocomplete-fortran:rebuild'}, 13 | {label:'Save external index file', command:'autocomplete-fortran:saveIndex'} 14 | ] 15 | ] 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "autocomplete-fortran", 3 | "main": "./lib/autocomplete-fortran", 4 | "version": "0.9.7", 5 | "description": "autocomplete for FORTRAN", 6 | "keywords": [ 7 | "FORTRAN", 8 | "autocomplete" 9 | ], 10 | "repository": "https://github.com/hansec/autocomplete-fortran", 11 | "license": "MIT", 12 | "engines": { 13 | "atom": "^1.0.0" 14 | }, 15 | "dependencies": {}, 16 | "providedServices": { 17 | "autocomplete.provider": { 18 | "versions": { 19 | "2.0.0": "provide" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /python/parse_fortran.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import re 3 | import json 4 | import sys 5 | from optparse import OptionParser 6 | # 7 | parser = OptionParser() 8 | parser.add_option("-s", "--std", 9 | action="store_true", dest="std", default=False, 10 | help="Read file from STDIN") 11 | parser.add_option("-d", "--debug", 12 | action="store_true", dest="debug", default=False, 13 | help="Print debug information") 14 | parser.add_option("-p", "--pretty", 15 | action="store_true", dest="pretty", default=False, 16 | help="Format JSON output") 17 | parser.add_option("--close_scopes", 18 | action="store_true", dest="close_scopes", default=False, 19 | help="Force all open scopes to close at end of parsing") 20 | parser.add_option("--fixed", 21 | action="store_true", dest="fixed", default=False, 22 | help="Parse using fixed-format rules") 23 | parser.add_option("--files", dest="files", default=None, 24 | help="Files to parse") 25 | (options, args) = parser.parse_args() 26 | debug = options.debug 27 | fixed_format = options.fixed 28 | # 29 | USE_REGEX = re.compile(r'[ \t]*USE[ \t:]*([a-z0-9_]*)', re.I) 30 | SUB_REGEX = re.compile(r'[ \t]*(PURE|ELEMENTAL|RECURSIVE)*[ \t]*(SUBROUTINE)', re.I) 31 | END_SUB_REGEX = re.compile(r'[ \t]*END[ \t]*SUBROUTINE', re.I) 32 | FUN_REGEX = re.compile(r'[ \t]*(PURE|ELEMENTAL|RECURSIVE)*[ \t]*(FUNCTION)', re.I) 33 | RESULT_REGEX = re.compile(r'RESULT[ ]*\(([a-z0-9_]*)\)', re.I) 34 | END_FUN_REGEX = re.compile(r'[ \t]*END[ \t]*FUNCTION', re.I) 35 | MOD_REGEX = re.compile(r'[ \t]*MODULE[ \t]*([a-z0-9_]*)', re.I) 36 | END_MOD_REGEX = re.compile(r'[ \t]*END[ \t]*MODULE', re.I) 37 | PROG_REGEX = re.compile(r'[ \t]*PROGRAM[ \t]*([a-z0-9_]*)', re.I) 38 | END_PROG_REGEX = re.compile(r'[ \t]*END[ \t]*PROGRAM', re.I) 39 | INT_REGEX = re.compile(r'[ \t]*(?:ABSTRACT)?[ \t]*INTERFACE[ \t]*([a-z0-9_]*)', re.I) 40 | END_INT_REGEX = re.compile(r'[ \t]*END[ \t]*INTERFACE', re.I) 41 | END_GEN_REGEX = re.compile(r'[ \t]*END[ \t]*$', re.I) 42 | TYPE_DEF_REGEX = re.compile(r'[ \t]*TYPE', re.I) 43 | EXTENDS_REGEX = re.compile(r'EXTENDS[ ]*\(([a-z0-9_]*)\)', re.I) 44 | END_TYPED_REGEX = re.compile(r'[ \t]*END[ \t]*TYPE', re.I) 45 | NAT_VAR_REGEX = re.compile(r'[ \t]*(INTEGER|REAL|DOUBLE PRECISION|COMPLEX|CHARACTER|LOGICAL|PROCEDURE|CLASS|TYPE)', re.I) 46 | KIND_SPEC_REGEX = re.compile(r'([ \t]*\([a-z0-9_ =*]*\)|\*[0-9]*)', re.I) 47 | KEYWORD_LIST_REGEX = re.compile(r'[ \t]*,[ \t]*(PUBLIC|PRIVATE|ALLOCATABLE|POINTER|TARGET|DIMENSION\([a-z0-9_:, ]*\)|OPTIONAL|INTENT\([inout]*\)|DEFERRED|NOPASS|SAVE|PARAMETER)', re.I) 48 | TATTR_LIST_REGEX = re.compile(r'[ \t]*,[ \t]*(PUBLIC|PRIVATE|ABSTRACT|EXTENDS\([a-z0-9_]*\))', re.I) 49 | VIS_REGEX = re.compile(r'(PUBLIC|PRIVATE)', re.I) 50 | WORD_REGEX = re.compile(r'[a-z][a-z0-9_]*', re.I) 51 | SUB_PAREN_MATCH = re.compile(r'\([a-z0-9_, ]*\)', re.I) 52 | KIND_SPEC_MATCH = re.compile(r'\([a-z0-9_, =*]*\)', re.I) 53 | # 54 | if fixed_format: 55 | COMMENT_LINE_MATCH = re.compile(r'(!|c|d|\*)') 56 | CONT_REGEX = re.compile(r'( [\S])') 57 | else: 58 | COMMENT_LINE_MATCH = re.compile(r'([ \t]*!)') 59 | CONT_REGEX = re.compile(r'([ \t]*&)') 60 | # 61 | def separate_def_list(test_str): 62 | paren_count=0 63 | def_list = [] 64 | curr_str = '' 65 | for char in test_str: 66 | if char == '(': 67 | paren_count += 1 68 | elif char == ')': 69 | paren_count -= 1 70 | elif char == ',' and paren_count==0: 71 | if curr_str != '': 72 | def_list.append(curr_str) 73 | curr_str = '' 74 | continue 75 | curr_str += char 76 | if curr_str != '': 77 | def_list.append(curr_str) 78 | return def_list 79 | # 80 | def get_var_dims(test_str): 81 | paren_count = 0 82 | curr_dim = 0 83 | for char in test_str: 84 | if char == '(': 85 | paren_count += 1 86 | if paren_count==1: 87 | curr_dim = 1 88 | elif char == ')': 89 | paren_count -= 1 90 | elif char == ',' and paren_count==1: 91 | curr_dim += 1 92 | return curr_dim 93 | # 94 | def parse_keywords(keywords): 95 | modifiers = [] 96 | for key in keywords: 97 | key_lower = key.lower() 98 | if key_lower == 'pointer': 99 | modifiers.append(1) 100 | elif key_lower == 'allocatable': 101 | modifiers.append(2) 102 | elif key_lower == 'optional': 103 | modifiers.append(3) 104 | elif key_lower == 'public': 105 | modifiers.append(4) 106 | elif key_lower == 'private': 107 | modifiers.append(5) 108 | elif key_lower == 'nopass': 109 | modifiers.append(6) 110 | elif key_lower.startswith('dimension'): 111 | ndims = key_lower.count(':') 112 | modifiers.append(20+ndims) 113 | modifiers.sort() 114 | return modifiers 115 | # 116 | def read_var_def(line, type_word=None): 117 | if type_word is None: 118 | type_match = NAT_VAR_REGEX.match(line) 119 | if type_match is None: 120 | return None 121 | else: 122 | type_word = type_match.group(0).strip() 123 | trailing_line = line[type_match.end(0):] 124 | else: 125 | trailing_line = line[len(type_word):] 126 | type_word = type_word.upper() 127 | trailing_line = trailing_line.split('!')[0] 128 | # 129 | kind_match = KIND_SPEC_REGEX.match(trailing_line) 130 | if kind_match is not None: 131 | type_word += kind_match.group(0).strip().lower() 132 | trailing_line = trailing_line[kind_match.end(0):] 133 | else: 134 | # Class and Type statements need a kind spec 135 | if type_word.lower() == 'class' or type_word.lower() == 'type': 136 | return None 137 | # Make sure next character is space or comma 138 | if trailing_line[0] != ' ' and trailing_line[0] != ',': 139 | return None 140 | # 141 | keyword_match = KEYWORD_LIST_REGEX.match(trailing_line) 142 | keywords = [] 143 | while (keyword_match is not None): 144 | keywords.append(keyword_match.group(0).replace(',',' ').strip().upper()) 145 | trailing_line = trailing_line[keyword_match.end(0):] 146 | keyword_match = KEYWORD_LIST_REGEX.match(trailing_line) 147 | # Check if function 148 | fun_def = read_fun_def(trailing_line, [type_word, keywords]) 149 | if fun_def is not None: 150 | return fun_def 151 | # 152 | line_split = trailing_line.split('::') 153 | if len(line_split) == 1: 154 | if len(keywords) > 0: 155 | return None 156 | else: 157 | trailing_line = line_split[0] 158 | else: 159 | trailing_line = line_split[1] 160 | # 161 | var_words = separate_def_list(trailing_line.strip()) 162 | # 163 | return 'var', [type_word, keywords, var_words] 164 | # 165 | def read_fun_def(line, return_type=None): 166 | fun_match = FUN_REGEX.match(line) 167 | if fun_match is None: 168 | return None 169 | # 170 | trailing_line = line[fun_match.end(0):].strip() 171 | trailing_line = trailing_line.split('!')[0] 172 | name_match = WORD_REGEX.match(trailing_line) 173 | if name_match is not None: 174 | name = name_match.group(0) 175 | trailing_line = trailing_line[name_match.end(0):].strip() 176 | else: 177 | return None 178 | # 179 | paren_match = SUB_PAREN_MATCH.match(trailing_line) 180 | if paren_match is not None: 181 | word_match = WORD_REGEX.findall(paren_match.group(0)) 182 | if word_match is not None: 183 | word_match = [word for word in word_match] 184 | args = ','.join(word_match) 185 | trailing_line = trailing_line[paren_match.end(0):] 186 | # 187 | return_var = None 188 | if return_type is None: 189 | trailing_line = trailing_line.strip() 190 | results_match = RESULT_REGEX.match(trailing_line) 191 | if results_match is not None: 192 | return_var = results_match.group(1).strip().lower() 193 | return 'fun', [name, args, [return_type, return_var]] 194 | # 195 | def read_sub_def(line): 196 | sub_match = SUB_REGEX.match(line) 197 | if sub_match is None: 198 | return None 199 | # 200 | trailing_line = line[sub_match.end(0):].strip() 201 | trailing_line = trailing_line.split('!')[0] 202 | name_match = WORD_REGEX.match(trailing_line) 203 | if name_match is not None: 204 | name = name_match.group(0) 205 | trailing_line = trailing_line[name_match.end(0):].strip() 206 | else: 207 | return None 208 | # 209 | paren_match = SUB_PAREN_MATCH.match(trailing_line) 210 | args = '' 211 | if paren_match is not None: 212 | word_match = WORD_REGEX.findall(paren_match.group(0)) 213 | if word_match is not None: 214 | word_match = [word for word in word_match] 215 | args = ','.join(word_match) 216 | trailing_line = trailing_line[paren_match.end(0):] 217 | return 'sub', [name, args] 218 | # 219 | def read_type_def(line): 220 | type_match = TYPE_DEF_REGEX.match(line) 221 | if type_match is None: 222 | return None 223 | trailing_line = line[type_match.end(0):] 224 | trailing_line = trailing_line.split('!')[0] 225 | # Parse keywords 226 | keyword_match = TATTR_LIST_REGEX.match(trailing_line) 227 | keywords = [] 228 | parent = None 229 | while (keyword_match is not None): 230 | keyword_strip = keyword_match.group(0).replace(',',' ').strip().upper() 231 | extend_match = EXTENDS_REGEX.match(keyword_strip) 232 | if extend_match is not None: 233 | parent = extend_match.group(1).lower() 234 | else: 235 | keywords.append(keyword_strip) 236 | # 237 | trailing_line = trailing_line[keyword_match.end(0):] 238 | keyword_match = TATTR_LIST_REGEX.match(trailing_line) 239 | # Get name 240 | line_split = trailing_line.split('::') 241 | if len(line_split) == 1: 242 | if len(keywords) > 0 and parent is None: 243 | return None 244 | else: 245 | if trailing_line.split('(')[0].strip().lower() == 'is': 246 | return None 247 | trailing_line = line_split[0] 248 | else: 249 | trailing_line = line_split[1] 250 | # 251 | word_match = WORD_REGEX.match(trailing_line.strip()) 252 | if word_match is not None: 253 | name = word_match.group(0) 254 | else: 255 | return None 256 | # 257 | return 'typ', [name, parent, keywords] 258 | # 259 | def read_mod_def(line): 260 | mod_match = MOD_REGEX.match(line) 261 | if mod_match is None: 262 | return None 263 | else: 264 | name = mod_match.group(1) 265 | if name.lower() == 'procedure': 266 | trailing_line = line[mod_match.end(1):] 267 | pro_names = [] 268 | line_split = trailing_line.split(',') 269 | for name in line_split: 270 | pro_names.append(name.strip().lower()) 271 | return 'int_pro', pro_names 272 | return 'mod', name 273 | # 274 | def read_prog_def(line): 275 | prog_match = PROG_REGEX.match(line) 276 | if prog_match is None: 277 | return None 278 | else: 279 | return 'prog', prog_match.group(1) 280 | # 281 | def read_int_def(line): 282 | int_match = INT_REGEX.match(line) 283 | if int_match is None: 284 | return None 285 | else: 286 | int_name = int_match.group(1).lower() 287 | if int_name == '': 288 | return None 289 | if int_name == 'assignment' or int_name == 'operator': 290 | return None 291 | return 'int', int_match.group(1) 292 | # 293 | def read_use_stmt(line): 294 | use_match = USE_REGEX.match(line) 295 | if use_match is None: 296 | return None 297 | else: 298 | trailing_line = line[use_match.end(0):].lower() 299 | use_mod = use_match.group(1) 300 | only_ind = trailing_line.find('only:') 301 | only_list = [] 302 | if only_ind > -1: 303 | only_split = trailing_line[only_ind+5:].split(',') 304 | for only_stmt in only_split: 305 | only_list.append(only_stmt.split('=>')[0].strip()) 306 | return 'use', [use_mod, only_list] 307 | # 308 | class fortran_scope: 309 | def __init__(self, line_number, name, enc_scope=None, args=None): 310 | self.sline = line_number 311 | self.eline = None 312 | self.name = name 313 | self.children = [] 314 | self.use = [] 315 | self.parent = None 316 | self.vis = 0 317 | self.args = args 318 | if enc_scope is not None: 319 | self.FQSN = enc_scope.lower() + "::" + self.name.lower() 320 | else: 321 | self.FQSN = self.name.lower() 322 | def set_visibility(self, new_vis): 323 | self.vis = new_vis 324 | def add_use(self, use_mod, only_list=[]): 325 | lower_only = [] 326 | for only in only_list: 327 | lower_only.append(only.lower()) 328 | self.use.append([use_mod.lower(), lower_only]) 329 | def set_parent(self, parent_type): 330 | self.parent = parent_type 331 | def add_child(self,child): 332 | self.children.append(child) 333 | def get_type(self): 334 | return -1 335 | def get_desc(self): 336 | return 'unknown' 337 | def is_optional(self): 338 | return False 339 | def end(self, line_number): 340 | self.eline = line_number 341 | def write_scope(self): 342 | scope_dict = {'name': self.name, 'type': self.get_type(), 'desc': self.get_desc(), 'fbound': [self.sline, self.eline], 'mem': []}#{}} 343 | if self.args is not None: 344 | arg_str = self.args 345 | if len(self.children) > 0: 346 | args_split = arg_str.split(',') 347 | for child in self.children: 348 | try: 349 | ind = args_split.index(child.name) 350 | except: 351 | continue 352 | if child.is_optional(): 353 | args_split[ind] = args_split[ind] + "=" + args_split[ind] 354 | arg_str = ",".join(args_split) 355 | scope_dict['args'] = arg_str 356 | if len(self.children) > 0: 357 | for child in self.children: 358 | scope_dict['mem'].append(child.name.lower()) 359 | if len(self.use) > 0: 360 | scope_dict['use'] = [] 361 | for use_stmt in self.use: 362 | scope_dict['use'].append(use_stmt) 363 | if self.parent is not None: 364 | scope_dict['parent'] = self.parent 365 | if self.vis == -1: 366 | scope_dict['vis'] = '-1' 367 | elif self.vis == 1: 368 | scope_dict['vis'] = '1' 369 | return scope_dict 370 | # 371 | class fortran_module(fortran_scope): 372 | def __init__(self, line_number, name, enc_scope=None, args=None): 373 | self.sline = line_number 374 | self.eline = None 375 | self.name = name 376 | self.children = [] 377 | self.use = [] 378 | self.parent = None 379 | self.vis = 0 380 | self.args = args 381 | if enc_scope is not None: 382 | self.FQSN = enc_scope.lower() + "::" + self.name.lower() 383 | else: 384 | self.FQSN = self.name.lower() 385 | def get_type(self): 386 | return 1 387 | def get_desc(self): 388 | return 'MODULE' 389 | # 390 | class fortran_program(fortran_scope): 391 | def __init__(self, line_number, name, enc_scope=None, args=None): 392 | self.sline = line_number 393 | self.eline = None 394 | self.name = name 395 | self.children = [] 396 | self.use = [] 397 | self.parent = None 398 | self.vis = 0 399 | self.args = args 400 | if enc_scope is not None: 401 | self.FQSN = enc_scope.lower() + "::" + self.name.lower() 402 | else: 403 | self.FQSN = self.name.lower() 404 | def get_type(self): 405 | return 1 406 | def get_desc(self): 407 | return 'PROGRAM' 408 | # 409 | class fortran_subroutine(fortran_scope): 410 | def __init__(self, line_number, name, enc_scope=None, args=None): 411 | self.sline = line_number 412 | self.eline = None 413 | self.name = name 414 | self.children = [] 415 | self.use = [] 416 | self.parent = None 417 | self.vis = 0 418 | self.args = args 419 | if enc_scope is not None: 420 | self.FQSN = enc_scope.lower() + "::" + self.name.lower() 421 | else: 422 | self.FQSN = self.name.lower() 423 | def get_type(self): 424 | return 2 425 | def get_desc(self): 426 | return 'SUBROUTINE' 427 | # 428 | class fortran_function(fortran_scope): 429 | def __init__(self, line_number, name, enc_scope=None, args=None, return_type=None, result_var=None): 430 | self.sline = line_number 431 | self.eline = None 432 | self.name = name 433 | self.children = [] 434 | self.use = [] 435 | self.result_var = result_var 436 | if return_type is not None: 437 | self.return_type = return_type[0] 438 | self.modifiers = parse_keywords(return_type[1]) 439 | else: 440 | self.return_type = None 441 | self.modifiers = [] 442 | self.parent = None 443 | self.vis = 0 444 | self.args = args 445 | if enc_scope is not None: 446 | self.FQSN = enc_scope.lower() + "::" + self.name.lower() 447 | else: 448 | self.FQSN = self.name.lower() 449 | def get_type(self): 450 | return 3 451 | def get_desc(self): 452 | desc = None 453 | if self.result_var is not None: 454 | result_var_lower = self.result_var.lower() 455 | for child in self.children: 456 | if child.name == result_var_lower: 457 | return child.get_desc() 458 | if self.return_type is not None: 459 | return self.return_type 460 | return 'FUNCTION' 461 | # 462 | class fortran_type(fortran_scope): 463 | def __init__(self, line_number, name, modifiers, enc_scope=None, args=None): 464 | self.sline = line_number 465 | self.eline = None 466 | self.name = name 467 | self.children = [] 468 | self.use = [] 469 | self.modifiers = modifiers 470 | self.parent = None 471 | self.vis = 0 472 | self.args = args 473 | if enc_scope is not None: 474 | self.FQSN = enc_scope.lower() + "::" + self.name.lower() 475 | else: 476 | self.FQSN = self.name.lower() 477 | for modifier in self.modifiers: 478 | if modifier == 4: 479 | self.vis = 1 480 | elif modifier == 5: 481 | self.vis = -1 482 | def get_type(self): 483 | return 4 484 | def get_desc(self): 485 | return 'TYPE' 486 | # 487 | class fortran_int(fortran_scope): 488 | def __init__(self, line_number, name, enc_scope=None, args=None): 489 | self.sline = line_number 490 | self.eline = None 491 | self.name = name 492 | self.children = [] 493 | self.use = [] 494 | self.parent = None 495 | self.vis = 0 496 | self.args = args 497 | if enc_scope is not None: 498 | self.FQSN = enc_scope.lower() + "::" + self.name.lower() 499 | else: 500 | self.FQSN = self.name.lower() 501 | def add_child(self, child_fqn): 502 | self.children.append(child_fqn) 503 | def get_type(self): 504 | return 5 505 | def get_desc(self): 506 | return 'INTERFACE' 507 | def write_scope(self): 508 | child_list = [] 509 | for child in self.children: 510 | child_list.append(child.lower()) 511 | scope_dict = {'name': self.name, 'type': 7, 'fbound': [self.sline, self.eline], 'mem': child_list} 512 | if self.vis == -1: 513 | scope_dict['vis'] = '-1' 514 | elif self.vis == 1: 515 | scope_dict['vis'] = '1' 516 | return scope_dict 517 | # 518 | class fortran_obj: 519 | def __init__(self, line_number, name, var_desc, modifiers, enc_scope=None, link_obj=None): 520 | self.sline = line_number 521 | self.name = name 522 | self.desc = var_desc 523 | self.modifiers = modifiers 524 | self.children = [] 525 | self.vis = 0 526 | if link_obj is not None: 527 | self.link_obj = link_obj.lower() 528 | else: 529 | self.link_obj = None 530 | if enc_scope is not None: 531 | self.FQSN = enc_scope.lower() + "::" + self.name.lower() 532 | else: 533 | self.FQSN = self.name.lower() 534 | for modifier in self.modifiers: 535 | if modifier == 4: 536 | self.vis = 1 537 | elif modifier == 5: 538 | self.vis = -1 539 | def set_visibility(self, new_vis): 540 | self.vis = new_vis 541 | def get_type(self): 542 | return 6 543 | def get_desc(self): 544 | return self.desc 545 | def set_dim(self,ndim): 546 | for (i,modifier) in enumerate(self.modifiers): 547 | if modifier > 20: 548 | self.modifiers[i] = ndim+20 549 | return 550 | self.modifiers.append(ndim+20) 551 | def is_optional(self): 552 | try: 553 | ind = self.modifiers.index(3) 554 | except: 555 | return False 556 | return True 557 | def write_scope(self): 558 | scope_dict = {'name': self.name, 'type': self.get_type(), 'fdef': self.sline, 'desc': self.get_desc()} 559 | if self.vis == -1: 560 | scope_dict['vis'] = '-1' 561 | elif self.vis == 1: 562 | scope_dict['vis'] = '1' 563 | if self.link_obj is not None: 564 | scope_dict['link'] = self.link_obj 565 | if len(self.modifiers) > 0: 566 | scope_dict['mods'] = self.modifiers 567 | return scope_dict 568 | # 569 | class fortran_file: 570 | def __init__(self, indent_level=None): 571 | self.global_dict = {} 572 | self.scope_list = [] 573 | self.variable_list = [] 574 | self.public_list = [] 575 | self.private_list = [] 576 | self.scope_stack = [] 577 | self.end_stack = [] 578 | self.current_scope = None 579 | self.END_REGEX = None 580 | self.enc_scope_name = None 581 | self.indent_level = indent_level 582 | def get_enc_scope_name(self): 583 | if self.current_scope is None: 584 | return None 585 | name_str = self.current_scope.name 586 | if len(self.scope_stack) > 0: 587 | for scope in reversed(self.scope_stack): 588 | name_str = scope.name + '::' + name_str 589 | return name_str 590 | def add_scope(self,new_scope, END_SCOPE_REGEX, hidden=False): 591 | if hidden: 592 | self.variable_list.append(new_scope) 593 | else: 594 | self.scope_list.append(new_scope) 595 | if self.current_scope is not None: 596 | self.current_scope.add_child(new_scope) 597 | self.scope_stack.append(self.current_scope) 598 | if self.END_REGEX is not None: 599 | self.end_stack.append(self.END_REGEX) 600 | self.current_scope = new_scope 601 | self.END_REGEX = END_SCOPE_REGEX 602 | self.enc_scope_name = self.get_enc_scope_name() 603 | def end_scope(self,line_number): 604 | self.current_scope.end(line_number) 605 | if len(self.scope_stack) > 0: 606 | self.current_scope = self.scope_stack.pop() 607 | else: 608 | self.current_scope = None 609 | if len(self.end_stack) > 0: 610 | self.END_REGEX = self.end_stack.pop() 611 | else: 612 | self.END_REGEX = None 613 | self.enc_scope_name = self.get_enc_scope_name() 614 | def add_variable(self,new_var): 615 | self.current_scope.add_child(new_var) 616 | self.variable_list.append(new_var) 617 | def add_int_member(self,key): 618 | self.current_scope.add_child(key) 619 | def add_private(self,name): 620 | self.private_list.append(self.enc_scope_name+'::'+name) 621 | def add_public(self,name): 622 | self.public_list.append(self.enc_scope_name+'::'+name) 623 | def add_use(self,mod_words): 624 | if len(mod_words) > 0: 625 | n = len(mod_words) 626 | if n > 2: 627 | use_list = mod_words[2:] 628 | self.current_scope.add_use(mod_words[0], use_list) 629 | else: 630 | self.current_scope.add_use(mod_words[0]) 631 | def dump_json(self, line_count, close_open=False): 632 | if (self.current_scope is not None) and (not close_open): 633 | print(json.dumps({'error': 'Scope stack not empty'})) 634 | return 635 | if close_open: 636 | while (self.current_scope is not None): 637 | self.end_scope(line_count) 638 | js_output = {'objs': {}, 'scopes': []} 639 | for scope in self.scope_list: 640 | js_output['objs'][scope.FQSN] = scope.write_scope() 641 | js_output['scopes'].append(scope.FQSN) 642 | for variable in self.variable_list: 643 | js_output['objs'][variable.FQSN] = variable.write_scope() 644 | for private_obj in self.private_list: 645 | if private_obj in js_output['objs']: 646 | js_output['objs'][private_obj]['vis'] = -1 647 | for public_obj in self.public_list: 648 | if public_obj in js_output['objs']: 649 | js_output['objs'][public_obj]['vis'] = 1 650 | print(json.dumps(js_output, indent=self.indent_level, separators=(',', ':'))) 651 | # 652 | def_tests = [read_var_def, read_sub_def, read_fun_def, read_type_def, read_use_stmt, read_int_def, read_mod_def, read_prog_def] 653 | # 654 | def process_file(filename,close_open_scopes): 655 | if filename == 'STDIN': 656 | f = sys.stdin 657 | close_open_scopes = True 658 | else: 659 | f = open(filename) 660 | indent_level = None 661 | if options.pretty: 662 | indent_level = 2 663 | file_obj = fortran_file(indent_level) 664 | line_number = 0 665 | next_line_num = 1 666 | at_eof = False 667 | next_line = None 668 | while(not at_eof): 669 | # Get next line 670 | if next_line is None: 671 | line = f.readline() 672 | else: 673 | line = next_line 674 | next_line = None 675 | line_number = next_line_num 676 | next_line_num = line_number + 1 677 | if line == '': 678 | break # Reached end of file 679 | # Skip comment lines 680 | match = COMMENT_LINE_MATCH.match(line) 681 | if (match is not None): 682 | continue 683 | # Merge lines with continuations 684 | if fixed_format: 685 | next_line = f.readline() 686 | cont_match = CONT_REGEX.match(next_line) 687 | while( cont_match is not None ): 688 | line = line.rstrip() + next_line[6:].strip() 689 | next_line_num += 1 690 | next_line = f.readline() 691 | cont_match = CONT_REGEX.match(next_line) 692 | else: 693 | iAmper = line.find('&') 694 | iComm = line.find('!') 695 | if iComm < 0: 696 | iComm = iAmper + 1 697 | while (iAmper >= 0 and iAmper < iComm): 698 | split_line = line.split('&') 699 | next_line = f.readline() 700 | if next_line == '': 701 | at_eof = True 702 | break # Reached end of file 703 | # Skip comment lines 704 | match = COMMENT_LINE_MATCH.match(next_line) 705 | if (match is not None): 706 | continue 707 | cont_match = CONT_REGEX.match(next_line) 708 | if cont_match is not None: 709 | next_line = next_line[cont_match.end(0):] 710 | next_line_num += 1 711 | line = split_line[0].rstrip() + ' ' + next_line.strip() 712 | iAmper = line.find('&') 713 | iComm = line.find('!') 714 | if iComm < 0: 715 | iComm = iAmper + 1 716 | next_line = None 717 | line = line.rstrip() 718 | # Test for scope end 719 | if file_obj.END_REGEX is not None: 720 | match = file_obj.END_REGEX.match(line) 721 | if (match is not None): 722 | file_obj.end_scope(line_number) 723 | if(debug): 724 | print('{1} !!! END scope({0})'.format(line_number, line.strip())) 725 | continue 726 | line_no_comment = line.split('!')[0] 727 | match = END_GEN_REGEX.match(line_no_comment) 728 | if (match is not None): 729 | file_obj.end_scope(line_number) 730 | if(debug): 731 | print('{1} !!! END scope({0})'.format(line_number, line.strip())) 732 | continue 733 | # Loop through tests 734 | obj_read = None 735 | for test in def_tests: 736 | obj_read = test(line) 737 | if obj_read is not None: 738 | break 739 | # 740 | if obj_read is not None: 741 | obj_type = obj_read[0] 742 | obj = obj_read[1] 743 | if obj_type == 'var': 744 | link_name = None 745 | if obj[0][:3] == 'PRO': 746 | if isinstance(file_obj.current_scope,fortran_int): 747 | for var_name in obj[2]: 748 | file_obj.add_int_member(var_name) 749 | if(debug): 750 | print('{1} !!! INTERFACE-PRO statement({0})'.format(line_number, line.strip())) 751 | continue 752 | i1 = obj[0].find('(') 753 | i2 = obj[0].find(')') 754 | if i1 > -1 and i2 > -1: 755 | link_name = obj[0][i1+1:i2] 756 | for var_name in obj[2]: 757 | if var_name.find('=>') > -1: 758 | name_split = var_name.split('=>') 759 | name_stripped = name_split[0] 760 | link_name = name_split[1].split('(')[0].strip() 761 | if link_name.lower() == 'null': 762 | link_name = None 763 | else: 764 | name_stripped = var_name.split('=')[0] 765 | var_dim = 0 766 | if name_stripped.find('(') > -1: 767 | var_dim = get_var_dims(name_stripped) 768 | name_stripped = name_stripped.split('(')[0].strip() 769 | modifiers = parse_keywords(obj[1]) 770 | new_var = fortran_obj(line_number, name_stripped, obj[0], modifiers, file_obj.enc_scope_name, link_name) 771 | if var_dim > 0: 772 | new_var.set_dim(var_dim) 773 | file_obj.add_variable(new_var) 774 | if(debug): 775 | print('{1} !!! VARIABLE statement({0})'.format(line_number, line.strip())) 776 | elif obj_type == 'mod': 777 | new_mod = fortran_module(line_number, obj, file_obj.enc_scope_name) 778 | file_obj.add_scope(new_mod, END_MOD_REGEX) 779 | if(debug): 780 | print('{1} !!! MODULE statement({0})'.format(line_number, line.strip())) 781 | elif obj_type == 'prog': 782 | new_prog = fortran_program(line_number, obj, file_obj.enc_scope_name) 783 | file_obj.add_scope(new_prog, END_PROG_REGEX) 784 | if(debug): 785 | print('{1} !!! PROGRAM statement({0})'.format(line_number, line.strip())) 786 | elif obj_type == 'sub': 787 | new_sub = fortran_subroutine(line_number, obj[0], file_obj.enc_scope_name, obj[1]) 788 | file_obj.add_scope(new_sub, END_SUB_REGEX) 789 | if(debug): 790 | print('{1} !!! SUBROUTINE statement({0})'.format(line_number, line.strip())) 791 | elif obj_type == 'fun': 792 | new_fun = fortran_function(line_number, obj[0], file_obj.enc_scope_name, obj[1], return_type=obj[2][0], result_var=obj[2][1]) 793 | file_obj.add_scope(new_fun, END_FUN_REGEX) 794 | if obj[2][0] is not None: 795 | new_obj = fortran_obj(line_number, obj[0], obj[2][0][0], obj[2][0][1], file_obj.enc_scope_name, None) 796 | file_obj.add_variable(new_obj) 797 | if(debug): 798 | print('{1} !!! FUNCTION statement({0})'.format(line_number, line.strip())) 799 | elif obj_type == 'typ': 800 | modifiers = parse_keywords(obj[2]) 801 | new_type = fortran_type(line_number, obj[0], modifiers, file_obj.enc_scope_name) 802 | if obj[1] is not None: 803 | new_type.set_parent(obj[1]) 804 | file_obj.add_scope(new_type, END_TYPED_REGEX) 805 | if(debug): 806 | print('{1} !!! TYPE statement({0})'.format(line_number, line.strip())) 807 | elif obj_type == 'int': 808 | new_int = fortran_int(line_number, obj, file_obj.enc_scope_name) 809 | file_obj.add_scope(new_int, END_INT_REGEX, True) 810 | if(debug): 811 | print('{1} !!! INTERFACE statement({0})'.format(line_number, line.strip())) 812 | elif obj_type == 'int_pro': 813 | if file_obj.current_scope is None: 814 | continue 815 | if not isinstance(file_obj.current_scope,fortran_int): 816 | continue 817 | for name in obj: 818 | file_obj.add_int_member(name) 819 | if(debug): 820 | print('{1} !!! INTERFACE-PRO statement({0})'.format(line_number, line.strip())) 821 | elif obj_type == 'use': 822 | file_obj.current_scope.add_use(obj[0], obj[1]) 823 | if(debug): 824 | print('{1} !!! USE statement({0})'.format(line_number, line.strip())) 825 | # Look for visiblity statement 826 | match = VIS_REGEX.match(line) 827 | if (match is not None): 828 | match_lower = match.group(0).lower() 829 | trailing_line = line[match.end(0):] 830 | mod_words = WORD_REGEX.findall(trailing_line) 831 | if len(mod_words) == 0: 832 | if match_lower == 'private': 833 | file_obj.current_scope.set_visibility(-1) 834 | else: 835 | if match_lower == 'private': 836 | for word in mod_words: 837 | file_obj.add_private(word) 838 | else: 839 | for word in mod_words: 840 | file_obj.add_public(word) 841 | if(debug): 842 | print('Found visiblity statement, {0}:{1}, {2}'.format(filename, line_number, line.strip())) 843 | continue 844 | f.close() 845 | file_obj.dump_json(line_number,close_open_scopes) 846 | # 847 | if options.std: 848 | files = ['STDIN'] 849 | else: 850 | files = options.files.split(',') 851 | # 852 | for (ifile,fname) in enumerate(files): 853 | filename = fname 854 | try: 855 | process_file(filename,options.close_scopes) 856 | except: 857 | if debug: 858 | raise 859 | print(json.dumps({'error': 'Python error'})) 860 | --------------------------------------------------------------------------------