├── .gitattributes ├── .gitignore ├── .paket ├── paket.bootstrapper.exe └── paket.targets ├── .travis.yml ├── FSharp.sublime-project ├── README.md ├── README.md_ ├── appveyor.yml ├── build.cmd ├── build.fsx ├── build.sh ├── paket.dependencies ├── paket.lock └── src ├── .no-sublime-package ├── Default.sublime-keymap ├── Support ├── FSharp Analyzer Output.hidden-tmLanguage ├── FSharp Analyzer Output.sublime-settings ├── FSharp Analyzer Output.sublime-syntax ├── FSharp Analyzer Output.tmTheme └── FSharp.tmLanguage ├── __init__.py ├── _init_.py ├── execute.py ├── fsac ├── __init__.py ├── client.py ├── fsac │ └── KEEPME ├── pipe_server.py ├── request.py ├── response.py └── server.py ├── fsharp.py ├── lib ├── __init__.py ├── editor.py ├── errors_panel.py ├── project.py ├── response_processor.py └── tooltips.py ├── subtrees └── plugin_lib │ ├── LICENSE.txt │ ├── README.md │ ├── __init__.py │ ├── collections.py │ ├── context.py │ ├── events.py │ ├── filter.py │ ├── fs_completion.py │ ├── io.py │ ├── panels.py │ ├── path.py │ ├── plat.py │ ├── settings.py │ ├── sublime.py │ ├── subprocess.py │ └── text.py ├── test_runner.py ├── tests ├── __init__.py └── lib │ ├── __init__.py │ ├── test_editor.py │ ├── test_events.py │ └── test_project.py └── xevents.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | text=auto 3 | 4 | # These files need to have LF as line terminator, even on Windows. 5 | *.tmSnippet text eol=lf 6 | *.*tmLanguage text eol=lf 7 | messages.json text eol=lf 8 | *.py text eol=lf 9 | *.yaml text eol=lf 10 | *.yml text eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | packages/ 2 | \.paket/paket.exe 3 | paket-files/ 4 | bin/ 5 | release/ 6 | 7 | *.sublime-workspace -------------------------------------------------------------------------------- /.paket/paket.bootstrapper.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/zarchive-sublime-fsharp-package/e8f29405b5ad1d452bc556ab4742fc9d9e84776b/.paket/paket.bootstrapper.exe -------------------------------------------------------------------------------- /.paket/paket.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | 7 | true 8 | $(MSBuildThisFileDirectory) 9 | $(MSBuildThisFileDirectory)..\ 10 | 11 | 12 | 13 | $(PaketToolsPath)paket.exe 14 | $(PaketToolsPath)paket.bootstrapper.exe 15 | "$(PaketExePath)" 16 | mono --runtime=v4.0.30319 $(PaketExePath) 17 | "$(PaketBootStrapperExePath)" 18 | mono --runtime=v4.0.30319 $(PaketBootStrapperExePath) 19 | 20 | $(MSBuildProjectDirectory)\paket.references 21 | $(MSBuildProjectFullPath).paket.references 22 | $(PaketCommand) restore --references-files "$(PaketReferences)" 23 | $(PaketBootStrapperCommand) 24 | 25 | RestorePackages; $(BuildDependsOn); 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | 3 | os: 4 | - linux 5 | # - osx 6 | 7 | sudo: required 8 | 9 | env: 10 | global: 11 | - PACKAGE="FSharp" 12 | - ST3P="$HOME/.config/sublime-text-3/Packages" 13 | matrix: 14 | - SUBLIME_TEXT_VERSION="3" 15 | 16 | mono: 17 | - latest 18 | 19 | before_install: 20 | - curl -OL https://raw.githubusercontent.com/randy3k/UnitTesting/master/sbin/travis.sh 21 | 22 | install: 23 | - sh travis.sh bootstrap 24 | - rm "$ST3P/$PACKAGE" # Because UnitTesting has created this for us. 25 | - ln -s "$PWD/src" "$ST3P/$PACKAGE" # Because this is the correct directory. 26 | - sh build.sh 27 | 28 | build: false 29 | 30 | script: 31 | - sh travis.sh run_tests 32 | 33 | notifications: 34 | email: false 35 | -------------------------------------------------------------------------------- /FSharp.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "build_systems": 3 | [ 4 | { 5 | "name": "Build FSharp", 6 | "working_dir": "${project_path}", 7 | 8 | "shell_cmd": "./build.sh", 9 | 10 | "windows": { 11 | "shell_cmd": ".\\build.cmd" 12 | }, 13 | 14 | "variants": [ 15 | { 16 | "name": "Publish Locally (Install)", 17 | "shell_cmd": "./build.sh install", 18 | 19 | "windows": { 20 | "shell_cmd": ".\\build.cmd install" 21 | }, 22 | }, 23 | 24 | { 25 | "name": "Test (All)", 26 | "working_dir": "${project_path}/src", 27 | "target": "run_fsharp_tests", 28 | }, 29 | 30 | { 31 | "name": "Test (This File Only)", 32 | "working_dir": "${project_path}/src", 33 | "target": "run_fsharp_tests", 34 | "active_file_only": true 35 | } 36 | ] 37 | } 38 | ], 39 | 40 | "settings": 41 | { 42 | "ensure_new_line_at_eof_on_save": true, 43 | "trim_trailing_white_space_on_save": true, 44 | "default_line_ending": "unix", 45 | "translate_tabs_to_spaces": true, 46 | }, 47 | 48 | "folders": 49 | [ 50 | { 51 | "path": ".", 52 | "follow_symlinks": true, 53 | "folder_exclude_patterns": [".svn", ".git", ".hg", "CVS", "bin", ".paket", "packages", "paket-files"] 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build status](https://ci.appveyor.com/api/projects/status/uuqaj61vyqwwxqe1/branch/master?svg=true)](https://ci.appveyor.com/project/guillermooo/sublime-fsharp-package/branch/master) [![Build Status](https://travis-ci.org/fsharp/sublime-fsharp-package.svg?branch=master)](https://travis-ci.org/fsharp/sublime-fsharp-package) 2 | 3 | # FSharp 4 | 5 | Code intelligence for **F# development** in **Sublime Text 3**. 6 | 7 | 8 | ## Features 9 | 10 | - Autocompletion 11 | - Live error checking 12 | - Tooltips 13 | - Go to definition 14 | - Show definitions in current file 15 | - Syntax highlighting 16 | - Runs F# scripts in output panel 17 | 18 | 19 | ## Main Keyboard Shortcuts 20 | 21 | Keyboard Shortcut | Action 22 | ------------------ | ------------- | 23 | Ctrl+., Ctrl+. | Show F# commands 24 | Ctrl+., Ctrl+o | Show last output panel 25 | Ctrl+k, Ctrl+i | Show tooltip for symbol 26 | F12 | Go to definition 27 | Ctrl+space | Open autocomplete list 28 | 29 | 30 | ## Building 31 | 32 | 33 | #### Cross-platform 34 | 35 | - Start Sublime Text 36 | - Press F7 37 | - Select **Build FSharp** 38 | 39 | 40 | #### Linux/Mac 41 | 42 | ```shell 43 | ./build.sh 44 | ``` 45 | 46 | 47 | #### Windows 48 | 49 | ```shell 50 | build.cmd 51 | ``` 52 | 53 | ## Installing 54 | 55 | 56 | The `install` task 57 | will publish the package 58 | to Sublime Text's *Data* directory, 59 | and restart Sublime Text if it is running. 60 | 61 | 62 | #### Cross-platform 63 | 64 | - Start Sublime Text 65 | - Press F7 66 | - Select **Build FSharp - Publish Locally (Install)** 67 | 68 | 69 | #### Linux/Mac 70 | 71 | ```shell 72 | ./build.sh install 73 | ``` 74 | 75 | 76 | #### Windows 77 | 78 | For full installations, 79 | run the following command: 80 | 81 | ```shell 82 | build.cmd install 83 | ``` 84 | 85 | For portable installations, 86 | you must pass along 87 | the data directory. 88 | 89 | ```shell 90 | build install sublimeDir="d:\Path\To\Sublime\Text\Data" 91 | ``` 92 | 93 | Optionally, you can set 94 | the `SUBLIME_TEXT_DATA` environment variable, 95 | which should point to the Sublime Text *Data* directory. 96 | If `SUBLIME_TEXT_DATA` is present, 97 | you don't need to pass the `sublimeDir` argument 98 | to the build script. 99 | 100 | 101 | ## Developing FSharp 102 | 103 | Pull requests to FSharp welcome! 104 | 105 | 106 | #### General Steps for Development 107 | 108 | * Clone this repository to any folder outside of Sublime Text's *Data* folder 109 | * Edit files as needed 110 | * Close Sublime Text 111 | * Install via `./build.[sh|cmd] install` 112 | * Restart Sublime Text 113 | * Run the tests via command palette: **Build FSharp: Test (All)** 114 | 115 | Maintainers 116 | ----------- 117 | 118 | Tha maintainers of this repository appointed by the F# Core Engineering Group are: 119 | 120 | - [Robin Neatherway](https://github.com/rneatherway), [Steffen Forkmann](http://github.com/forki), [Karl Nilsson](http://github.com/kjnilsson), [Dave Thomas](http://github.com/7sharp9) and [Guillermo López-Anglada](http://github.com/guillermooo) 121 | - The primary maintainer for this repository is [Guillermo López-Anglada](http://github.com/guillermooo) 122 | -------------------------------------------------------------------------------- /README.md_: -------------------------------------------------------------------------------- 1 | [![Build status](https://ci.appveyor.com/api/projects/status/uuqaj61vyqwwxqe1/branch/master?svg=true)](https://ci.appveyor.com/project/guillermooo/sublime-fsharp-package/branch/master) [![Build Status](https://travis-ci.org/fsharp/sublime-fsharp-package.svg?branch=master)](https://travis-ci.org/fsharp/sublime-fsharp-package) 2 | 3 | # FSharp 4 | 5 | Code intelligence for **F# development** in **Sublime Text 3**. 6 | 7 | This repository if for releases only. Check out our [development repository][1]. 8 | 9 | [1]: https://github.com/fsharp/sublime-fsharp-package 10 | 11 | 12 | ## Features 13 | 14 | - Autocompletion 15 | - Live error checking 16 | - Tooltips 17 | - Go to definition 18 | - Show definitions in current file 19 | - Syntax highlighting 20 | - Runs F# scripts in output panel 21 | 22 | 23 | ## Main Keyboard Shortcuts 24 | 25 | Keyboard Shortcut | Action 26 | ------------------ | ------------- | 27 | Ctrl+., Ctrl+. | Show F# commands 28 | Ctrl+., Ctrl+o | Show last output panel 29 | Ctrl+k, Ctrl+i | Show tooltip for symbol 30 | F12 | Go to definition 31 | Ctrl+space | Open autocomplete list 32 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | global: 3 | PACKAGE: "FSharp" 4 | 5 | matrix: 6 | - SUBLIME_TEXT_VERSION : "3" 7 | 8 | install: 9 | # TODO: Use Desired state configuration or a block script. 10 | - ps: start-filedownload "https://raw.githubusercontent.com/randy3k/UnitTesting/master/sbin/appveyor.ps1" 11 | - ps: .\appveyor.ps1 "bootstrap" -verbose 12 | - ps: remove-item -recurse -force c:\st\data\packages\$env:PACKAGE\* 13 | - ps: .\build.cmd install sublimeDir=c:\st\data 14 | 15 | build: off 16 | 17 | test_script: 18 | - ps: .\appveyor.ps1 "run_tests" -verbose -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | cls 3 | 4 | .paket\paket.bootstrapper.exe 5 | if errorlevel 1 ( 6 | exit /b %errorlevel% 7 | ) 8 | 9 | .paket\paket.exe restore 10 | if errorlevel 1 ( 11 | exit /b %errorlevel% 12 | ) 13 | 14 | packages\FAKE\tools\FAKE.exe build.fsx %* 15 | -------------------------------------------------------------------------------- /build.fsx: -------------------------------------------------------------------------------- 1 | #r "packages/FAKE/tools/FakeLib.dll" // include Fake lib 2 | #r "System.Management" 3 | 4 | open System 5 | open System.Net 6 | open System.IO 7 | open System.Management 8 | open Fake 9 | open Fake.Git 10 | open Fake.ProcessHelper 11 | 12 | let releaseRepo = "https://github.com/guillermooo/sublime-fsharp-package-releases.git" 13 | 14 | // build parameters 15 | let dataDir = getBuildParam "sublimeDir" 16 | 17 | ProcessHelper.killCreatedProcesses <- false 18 | 19 | let isWindows = (Path.DirectorySeparatorChar = '\\') 20 | let sublimeDataPath () = 21 | let UnixPaths = 22 | [ (Environment.GetEnvironmentVariable("HOME") + "/Library/Application Support/Sublime Text 3") 23 | (Environment.GetEnvironmentVariable("HOME") + "/.config/sublime-text-3") ] 24 | 25 | let WindowsPaths = 26 | // Non-standard variable. It saves time while developing. 27 | [ Environment.GetEnvironmentVariable("SUBLIME_TEXT_DATA") 28 | Environment.ExpandEnvironmentVariables(@"%APPDATA%\Sublime Text 3") ] 29 | 30 | let searchPaths = if isWindows then WindowsPaths else UnixPaths 31 | let directories = 32 | searchPaths 33 | |> List.filter Directory.Exists 34 | 35 | match directories.Length with 36 | | 0 -> trace "No Sublime text 3 installation found" 37 | exit 1 38 | | _ -> directories.Head 39 | 40 | let getSublimeStartArgs () = 41 | if not isWindows then 42 | let isRunning = getProcessesByName "Sublime Text" |> Seq.length > 0 43 | if isRunning then Some("open", "-a \"Sublime Text\"") else None 44 | else 45 | let query = "SELECT CommandLine FROM Win32_Process WHERE Name LIKE '%sublime_text%'" 46 | use searcher = new System.Management.ManagementObjectSearcher(query); 47 | searcher.Get () 48 | |> Seq.cast 49 | |> Seq.tryFind (fun (mo:ManagementObject) -> true) 50 | |> Option.map (fun mo -> mo.["CommandLine"] |> string, "") 51 | 52 | let startSublime (startArgs: (string*string) option) = 53 | startArgs |> Option.iter(fun (file,args) -> 54 | fireAndForget (fun info -> 55 | info.FileName <- file 56 | info.Arguments <- args 57 | ) 58 | ) 59 | 60 | let killSublime () = 61 | let proc = if isWindows then "sublime_text" else "Sublime Text" 62 | killProcess proc 63 | 64 | Target "Clean" (fun _ -> 65 | DeleteDirs ["bin"; "release"] 66 | ) 67 | 68 | Target "Build" (fun _ -> 69 | CreateDir "bin" 70 | CopyRecursive "src" "bin" true |> ignore 71 | Unzip "bin/fsac/fsac" "paket-files/github.com/fsautocomplete.zip" 72 | ) 73 | 74 | Target "Install" (fun _ -> 75 | let startArgs = getSublimeStartArgs () 76 | killSublime () 77 | let sublimePath = if (not (String.IsNullOrWhiteSpace dataDir)) && Directory.Exists dataDir then dataDir else sublimeDataPath () 78 | trace sublimePath 79 | let target = Path.Combine(sublimePath, "Packages/FSharp") 80 | CopyRecursive "bin" target true |> ignore 81 | startSublime startArgs 82 | ) 83 | 84 | Target "Release" (fun _ -> 85 | let tag = getBuildParam "tag" 86 | if String.IsNullOrEmpty tag then 87 | failwith "please provide a tag as 'tag=x.x.x'" 88 | CreateDir "release" 89 | Repository.clone "release" releaseRepo "." 90 | Repository.fullclean "release" 91 | CopyRecursive "bin" "release" true |> ignore 92 | DeleteDirs ["release/tests"] 93 | DeleteFile "release/test_runner.py" 94 | CopyFile "release/README.md" "README.md_" 95 | StageAll "release" 96 | Commit "release" (sprintf "new version %s" tag) 97 | Branches.tag "release" tag 98 | Branches.push "release" 99 | Branches.pushTag "release" releaseRepo tag 100 | Branches.tag "." tag 101 | Branches.pushTag "." "origin" tag 102 | ) 103 | 104 | "Clean" 105 | ==> "Build" 106 | 107 | "Build" 108 | ==> "Install" 109 | 110 | "Build" 111 | ==> "Release" 112 | 113 | RunTargetOrDefault "Build" 114 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if ! (test "command -v fsharpc" || test "command -v fsc") 4 | then 5 | echo "Error: F# compiler not found. Is it installed?" >&2 6 | exit 1 7 | fi 8 | 9 | if test "$OS" = "Windows_NT" 10 | then 11 | # use .Net 12 | 13 | .paket/paket.bootstrapper.exe 14 | exit_code=$? 15 | if [ $exit_code -ne 0 ]; then 16 | exit $exit_code 17 | fi 18 | 19 | .paket/paket.exe restore 20 | exit_code=$? 21 | if [ $exit_code -ne 0 ]; then 22 | exit $exit_code 23 | fi 24 | 25 | [ ! -e build.fsx ] && .paket/paket.exe update 26 | packages/FAKE/tools/FAKE.exe $@ --fsiargs -d:MONO build.fsx 27 | else 28 | # use mono 29 | mono .paket/paket.bootstrapper.exe 30 | exit_code=$? 31 | if [ $exit_code -ne 0 ]; then 32 | exit $exit_code 33 | fi 34 | 35 | mono .paket/paket.exe restore 36 | exit_code=$? 37 | if [ $exit_code -ne 0 ]; then 38 | exit $exit_code 39 | fi 40 | 41 | [ ! -e build.fsx ] && mono .paket/paket.exe update 42 | mono packages/FAKE/tools/FAKE.exe $@ --fsiargs -d:MONO build.fsx 43 | fi 44 | -------------------------------------------------------------------------------- /paket.dependencies: -------------------------------------------------------------------------------- 1 | source https://nuget.org/api/v2 2 | 3 | nuget FAKE 4 | http https://github.com/fsharp/FSharp.AutoComplete/releases/download/0.18.2/fsautocomplete.zip 5 | -------------------------------------------------------------------------------- /paket.lock: -------------------------------------------------------------------------------- 1 | NUGET 2 | remote: https://nuget.org/api/v2 3 | specs: 4 | FAKE (3.35.2) 5 | HTTP 6 | remote: https://github.com 7 | specs: 8 | fsautocomplete.zip (/fsharp/FSharp.AutoComplete/releases/download/0.18.2/fsautocomplete.zip) -------------------------------------------------------------------------------- /src/.no-sublime-package: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/zarchive-sublime-fsharp-package/e8f29405b5ad1d452bc556ab4742fc9d9e84776b/src/.no-sublime-package -------------------------------------------------------------------------------- /src/Default.sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | 3 | {"keys": ["ctrl+k", "ctrl+i"], "command": "fs_run_fsac", "args": { "cmd": "tooltip" }, "context": [{ "key": "fs_is_code_file"}, {"key": "setting.command_mode", "operand": false}]}, 4 | {"keys": ["f12"], "command": "fs_run_fsac", "args": { "cmd": "finddecl"}, "context": [{ "key": "fs_is_code_file"}, {"key": "setting.command_mode", "operand": false}]}, 5 | {"keys": ["ctrl+space"], "command": "fs_run_fsac", "args": { "cmd": "completion" }, 6 | "context": [ 7 | {"key": "fs_is_code_file"}, 8 | {"key": "selector", "operator": "not_equal", "operand": "string, comment"}, 9 | {"key": "setting.command_mode", "operand": false} 10 | ]}, 11 | 12 | {"keys": ["ctrl+.", "ctrl+."], "command": "fs_show_options", "context": [{ "key": "setting.command_mode", "operand": false }]}, 13 | {"keys": ["ctrl+.", "ctrl+o"], "command": "show_panel", "args": {"panel": "output.fs.out", "toggle": true}, "context": [{"key": "setting.command_mode", "operand": false}]} 14 | ] -------------------------------------------------------------------------------- /src/Support/FSharp Analyzer Output.hidden-tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | FSharp Analysis Output 7 | patterns 8 | 9 | 10 | captures 11 | 12 | 1 13 | 14 | name 15 | analysis.severity.error.dart-analysis-output 16 | 17 | 10 18 | 19 | name 20 | field.separator.dart-analysis-output 21 | 22 | 11 23 | 24 | name 25 | message.dart-analysis-output 26 | 27 | 2 28 | 29 | name 30 | field.separator.dart-analysis-output 31 | 32 | 3 33 | 34 | name 35 | analysis.type.dart-analysis-output 36 | 37 | 4 38 | 39 | name 40 | field.separator.dart-analysis-output 41 | 42 | 5 43 | 44 | name 45 | file.name.dart-analysis-output 46 | 47 | 6 48 | 49 | name 50 | field.separator.dart-analysis-output 51 | 52 | 7 53 | 54 | name 55 | row.dart-analysis-output 56 | 57 | 8 58 | 59 | name 60 | field.separator.dart-analysis-output 61 | 62 | 9 63 | 64 | name 65 | column.dart-analysis-output 66 | 67 | 68 | match 69 | ^(ERROR)(\|)(\w+)(\|)(.+)(\|)(\d+)(\|)(\d+)(\|)(.+)$ 70 | name 71 | analysis.result.dart-analysis-output 72 | 73 | 74 | captures 75 | 76 | 1 77 | 78 | name 79 | analysis.severity.warning.dart-analysis-output 80 | 81 | 10 82 | 83 | name 84 | field.separator.dart-analysis-output 85 | 86 | 11 87 | 88 | name 89 | message.dart-analysis-output 90 | 91 | 2 92 | 93 | name 94 | field.separator.dart-analysis-output 95 | 96 | 3 97 | 98 | name 99 | analysis.type.dart-analysis-output 100 | 101 | 4 102 | 103 | name 104 | field.separator.dart-analysis-output 105 | 106 | 5 107 | 108 | name 109 | file.name.dart-analysis-output 110 | 111 | 6 112 | 113 | name 114 | field.separator.dart-analysis-output 115 | 116 | 7 117 | 118 | name 119 | row.dart-analysis-output 120 | 121 | 8 122 | 123 | name 124 | field.separator.dart-analysis-output 125 | 126 | 9 127 | 128 | name 129 | column.dart-analysis-output 130 | 131 | 132 | match 133 | ^(WARNING)(\|)(\w+)(\|)(.+)(\|)(\d+)(\|)(\d+)(\|)(.+)$ 134 | name 135 | analysis.result.dart-analysis-output 136 | 137 | 138 | captures 139 | 140 | 1 141 | 142 | name 143 | analysis.severity.info.dart-analysis-output 144 | 145 | 10 146 | 147 | name 148 | field.separator.dart-analysis-output 149 | 150 | 11 151 | 152 | name 153 | message.dart-analysis-output 154 | 155 | 2 156 | 157 | name 158 | field.separator.dart-analysis-output 159 | 160 | 3 161 | 162 | name 163 | analysis.type.dart-analysis-output 164 | 165 | 4 166 | 167 | name 168 | field.separator.dart-analysis-output 169 | 170 | 5 171 | 172 | name 173 | file.name.dart-analysis-output 174 | 175 | 6 176 | 177 | name 178 | field.separator.dart-analysis-output 179 | 180 | 7 181 | 182 | name 183 | row.dart-analysis-output 184 | 185 | 8 186 | 187 | name 188 | field.separator.dart-analysis-output 189 | 190 | 9 191 | 192 | name 193 | column.dart-analysis-output 194 | 195 | 196 | match 197 | ^(INFO)(\|)(\w+)(\|)(.+)(\|)(\d+)(\|)(\d+)(\|)(.+)$ 198 | name 199 | analysis.result.dart-analysis-output 200 | 201 | 202 | captures 203 | 204 | 1 205 | 206 | name 207 | analysis.severity.unknown.dart-analysis-output 208 | 209 | 10 210 | 211 | name 212 | field.separator.dart-analysis-output 213 | 214 | 11 215 | 216 | name 217 | message.dart-analysis-output 218 | 219 | 2 220 | 221 | name 222 | field.separator.dart-analysis-output 223 | 224 | 3 225 | 226 | name 227 | analysis.type.dart-analysis-output 228 | 229 | 4 230 | 231 | name 232 | field.separator.dart-analysis-output 233 | 234 | 5 235 | 236 | name 237 | file.name.dart-analysis-output 238 | 239 | 6 240 | 241 | name 242 | field.separator.dart-analysis-output 243 | 244 | 7 245 | 246 | name 247 | row.dart-analysis-output 248 | 249 | 8 250 | 251 | name 252 | field.separator.dart-analysis-output 253 | 254 | 9 255 | 256 | name 257 | column.dart-analysis-output 258 | 259 | 260 | match 261 | ^(\w+)(\|)(\w+)(\|)(.+)(\|)(\d+)(\|)(\d+)(\|)(.+)$ 262 | name 263 | analysis.result.dart-analysis-output 264 | 265 | 266 | scopeName 267 | text.dart-analysis-output 268 | 269 | 270 | -------------------------------------------------------------------------------- /src/Support/FSharp Analyzer Output.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | "color_scheme": "Packages/FSharp/FSharp Analyzer Output.tmTheme", 3 | "rulers": [], 4 | "gutter": false 5 | } -------------------------------------------------------------------------------- /src/Support/FSharp Analyzer Output.sublime-syntax: -------------------------------------------------------------------------------- 1 | %YAML 1.2 2 | --- 3 | name: FSharp Analyzer Output 4 | hidden: true 5 | scope: text.fsharp-analysis-output 6 | 7 | contexts: 8 | main: 9 | - match: ^(ERROR)(\|)(\w+)(\|)(.+)(\|)(\d+)(\|)(\d+)(\|)(.+)$ 10 | scope: analysis.result.fsharp-analysis-output 11 | captures: 12 | 1: analysis.severity.error.fsharp-analysis-output 13 | 2: field.separator.fsharp-analysis-output 14 | 3: analysis.type.fsharp-analysis-output 15 | 4: field.separator.fsharp-analysis-output 16 | 5: file.name.fsharp-analysis-output 17 | 6: field.separator.fsharp-analysis-output 18 | 7: row.fsharp-analysis-output 19 | 8: field.separator.fsharp-analysis-output 20 | 9: column.fsharp-analysis-output 21 | 10: field.separator.fsharp-analysis-output 22 | 11: message.fsharp-analysis-output 23 | - match: ^(WARNING)(\|)(\w+)(\|)(.+)(\|)(\d+)(\|)(\d+)(\|)(.+)$ 24 | scope: analysis.result.fsharp-analysis-output 25 | captures: 26 | 1: analysis.severity.warning.fsharp-analysis-output 27 | 2: field.separator.fsharp-analysis-output 28 | 3: analysis.type.fsharp-analysis-output 29 | 4: field.separator.fsharp-analysis-output 30 | 5: file.name.fsharp-analysis-output 31 | 6: field.separator.fsharp-analysis-output 32 | 7: row.fsharp-analysis-output 33 | 8: field.separator.fsharp-analysis-output 34 | 9: column.fsharp-analysis-output 35 | 10: field.separator.fsharp-analysis-output 36 | 11: message.fsharp-analysis-output 37 | - match: ^(INFO)(\|)(\w+)(\|)(.+)(\|)(\d+)(\|)(\d+)(\|)(.+)$ 38 | scope: analysis.result.fsharp-analysis-output 39 | captures: 40 | 1: analysis.severity.info.fsharp-analysis-output 41 | 2: field.separator.fsharp-analysis-output 42 | 3: analysis.type.fsharp-analysis-output 43 | 4: field.separator.fsharp-analysis-output 44 | 5: file.name.fsharp-analysis-output 45 | 6: field.separator.fsharp-analysis-output 46 | 7: row.fsharp-analysis-output 47 | 8: field.separator.fsharp-analysis-output 48 | 9: column.fsharp-analysis-output 49 | 10: field.separator.fsharp-analysis-output 50 | 11: message.fsharp-analysis-output 51 | - match: ^(\w+)(\|)(\w+)(\|)(.+)(\|)(\d+)(\|)(\d+)(\|)(.+)$ 52 | scope: analysis.result.fsharp-analysis-output 53 | captures: 54 | 1: analysis.severity.unknown.fsharp-analysis-output 55 | 2: field.separator.fsharp-analysis-output 56 | 3: analysis.type.fsharp-analysis-output 57 | 4: field.separator.fsharp-analysis-output 58 | 5: file.name.fsharp-analysis-output 59 | 6: field.separator.fsharp-analysis-output 60 | 7: row.fsharp-analysis-output 61 | 8: field.separator.fsharp-analysis-output 62 | 9: column.fsharp-analysis-output 63 | 10: field.separator.fsharp-analysis-output 64 | 11: message.fsharp-analysis-output 65 | -------------------------------------------------------------------------------- /src/Support/FSharp Analyzer Output.tmTheme: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Dart Analysis Ouput 7 | settings 8 | 9 | 10 | settings 11 | 12 | background 13 | #0F0F0F 14 | foreground 15 | #F0F0F0 16 | selection 17 | #383838 18 | 19 | 20 | 21 | name 22 | Secondary 23 | scope 24 | row, column, field.separator, file.name 25 | settings 26 | 27 | foreground 28 | #75715E 29 | 30 | 31 | 32 | name 33 | Type 34 | scope 35 | analysis.type 36 | settings 37 | 38 | foreground 39 | #548AE8 40 | 41 | 42 | 43 | name 44 | Severity - Error 45 | scope 46 | analysis.severity.error 47 | settings 48 | 49 | foreground 50 | #FF2E2E 51 | 52 | 53 | 54 | name 55 | Severity - Warning 56 | scope 57 | analysis.severity.warning 58 | settings 59 | 60 | foreground 61 | #FF7B1C 62 | 63 | 64 | 65 | name 66 | Severity - Info 67 | scope 68 | analysis.severity.info 69 | settings 70 | 71 | foreground 72 | #FCE535 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/Support/FSharp.tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | fileTypes 6 | 7 | fs 8 | fsi 9 | fsx 10 | 11 | foldingStartMarker 12 | 13 | foldingStopMarker 14 | 15 | name 16 | F# 17 | patterns 18 | 19 | 20 | include 21 | #comments 22 | 23 | 24 | include 25 | #type-abbreviation 26 | 27 | 28 | include 29 | #type-generic 30 | 31 | 32 | include 33 | #constants 34 | 35 | 36 | include 37 | #structure 38 | 39 | 40 | include 41 | #attributes 42 | 43 | 44 | include 45 | #characters 46 | 47 | 48 | include 49 | #strings 50 | 51 | 52 | include 53 | #definition 54 | 55 | 56 | include 57 | #method_calls 58 | 59 | 60 | include 61 | #modules 62 | 63 | 64 | include 65 | #anonymous_functions 66 | 67 | 68 | include 69 | #keywords 70 | 71 | 72 | repository 73 | 74 | anonymous_functions 75 | 76 | patterns 77 | 78 | 79 | begin 80 | \b(fun)\b 81 | beginCaptures 82 | 83 | 1 84 | 85 | name 86 | keyword.other.function-definition.fsharp 87 | 88 | 89 | end 90 | (->) 91 | endCaptures 92 | 93 | 1 94 | 95 | name 96 | keyword.other.fsharp 97 | 98 | 99 | name 100 | meta.function.anonymous 101 | patterns 102 | 103 | 104 | include 105 | #variables 106 | 107 | 108 | 109 | 110 | 111 | async-workflows 112 | 113 | patterns 114 | 115 | 116 | begin 117 | (async)(?=\s+\{) 118 | beginCaptures 119 | 120 | 1 121 | 122 | name 123 | storage.type.fsharp 124 | 125 | 126 | end 127 | (\}) 128 | name 129 | meta.sequence.definition.fsharp 130 | patterns 131 | 132 | 133 | include 134 | $self 135 | 136 | 137 | 138 | 139 | 140 | attributes 141 | 142 | patterns 143 | 144 | 145 | begin 146 | \[\< 147 | end 148 | \>\] 149 | name 150 | support.function.attribute.fsharp 151 | patterns 152 | 153 | 154 | include 155 | $self 156 | 157 | 158 | 159 | 160 | 161 | characters 162 | 163 | patterns 164 | 165 | 166 | begin 167 | (?<!\w)(') 168 | beginCaptures 169 | 170 | 1 171 | 172 | name 173 | punctuation.definition.string.begin.fsharp 174 | 175 | 176 | end 177 | (') 178 | endCaptures 179 | 180 | 1 181 | 182 | name 183 | punctuation.definition.string.end.fsharp 184 | 185 | 186 | name 187 | string.quoted.single.fsharp 188 | patterns 189 | 190 | 191 | name 192 | constant.character.fsharp 193 | 194 | 195 | include 196 | #string-escapes 197 | 198 | 199 | captures 200 | 201 | 1 202 | 203 | name 204 | invalid.illegal.escape.fsharp 205 | 206 | 207 | match 208 | .([^']*) 209 | 210 | 211 | 212 | 213 | 214 | comments 215 | 216 | patterns 217 | 218 | 219 | captures 220 | 221 | 1 222 | 223 | name 224 | comment.block.empty.fsharp 225 | 226 | 227 | match 228 | \(\*\*?(\*)\) 229 | name 230 | comment.block.fsharp 231 | 232 | 233 | begin 234 | \(\*[^\)] 235 | end 236 | \*\) 237 | name 238 | comment.block.fsharp 239 | patterns 240 | 241 | 242 | include 243 | #comments 244 | 245 | 246 | 247 | 248 | match 249 | \/\/(?:(?!\*\)).)*$ 250 | name 251 | comment.line.double-slash.fsharp 252 | 253 | 254 | 255 | constants 256 | 257 | patterns 258 | 259 | 260 | match 261 | \b(true|false)\b 262 | name 263 | constant.language.boolean.fsharp 264 | 265 | 266 | match 267 | \b(None|Some)\b 268 | name 269 | constant.language.unit.fsharp 270 | 271 | 272 | match 273 | \(\) 274 | name 275 | constant.language.option.fsharp 276 | 277 | 278 | match 279 | \b-?[0-9][0-9_]*((\.([0-9][0-9_]*([eE][+-]??[0-9][0-9_]*)?)?)|([eE][+-]??[0-9][0-9_]*)) 280 | name 281 | constant.numeric.floating-point.fsharp 282 | 283 | 284 | match 285 | \b(-?((0(x|X)[0-9a-fA-F][0-9a-fA-F_]*)|(0(o|O)[0-7][0-7_]*)|(0(b|B)[01][01_]*)|([0-9][0-9_]*))) 286 | name 287 | constant.numeric.integer.nativeint.fsharp 288 | 289 | 290 | 291 | definition 292 | 293 | patterns 294 | 295 | 296 | captures 297 | 298 | 1 299 | 300 | name 301 | storage.type.function.fsharp 302 | 303 | 3 304 | 305 | name 306 | keyword.operator.assignment.fsharp 307 | 308 | 309 | match 310 | \b(let!?)\s+(?:[_a-zA-Z][a-zA-Z_,]*|``.*``)(?:\s*,\s*(?:[_a-zA-Z][a-zA-Z_,]*|``.*``))*\s*(=) 311 | name 312 | meta.expression.fsharp 313 | 314 | 315 | begin 316 | (let)\s+(\(\|) 317 | beginCaptures 318 | 319 | 1 320 | 321 | name 322 | storage.type.function.fsharp 323 | 324 | 325 | end 326 | (=)|$ 327 | endCaptures 328 | 329 | 1 330 | 331 | name 332 | keyword.operator.assignment.fsharp 333 | 334 | 335 | name 336 | meta.active-expression.fsharp 337 | patterns 338 | 339 | 340 | captures 341 | 342 | 1 343 | 344 | name 345 | constant.other.fsharp 346 | 347 | 3 348 | 349 | name 350 | constant.other.fsharp 351 | 352 | 353 | match 354 | ([a-zA-Z0-9_]+?)(\|\))|([a-zA-Z0-9_]+?)(\|) 355 | name 356 | meta.active-expression.fsharp 357 | 358 | 359 | include 360 | #variables 361 | 362 | 363 | 364 | 365 | begin 366 | \b(ref|val|val|let|and|member|override|use)\s?(rec|inline|mutable)?(\s+\(?([a-zA-Z.\|_][a-zA-Z0-9.|_]*)\)?\w*)\b 367 | beginCaptures 368 | 369 | 1 370 | 371 | name 372 | storage.type.function.fsharp 373 | 374 | 2 375 | 376 | name 377 | keyword.other.function-recursive.fsharp 378 | 379 | 3 380 | 381 | name 382 | entity.name.function.fsharp 383 | 384 | 385 | end 386 | (=)|$ 387 | endCaptures 388 | 389 | 1 390 | 391 | name 392 | keyword.operator.assignment.fsharp 393 | 394 | 395 | name 396 | meta.binding.fsharp 397 | patterns 398 | 399 | 400 | include 401 | #variables 402 | 403 | 404 | 405 | 406 | include 407 | #sequences 408 | 409 | 410 | include 411 | #async-workflows 412 | 413 | 414 | 415 | keywords 416 | 417 | patterns 418 | 419 | 420 | begin 421 | ^\s*#\s*(light|if|else|endif|r|I|load|time|help|quit)\b 422 | end 423 | (\s|$) 424 | name 425 | meta.preprocessor.fsharp 426 | 427 | 428 | match 429 | \byield!? 430 | name 431 | keyword.other.fsharp 432 | 433 | 434 | match 435 | \b(try|finally|new|in|as|if|then|else|elif|raise|for|begin|end|match|with|when|type|inherit|null|do|while)\b 436 | name 437 | keyword.other.fsharp 438 | 439 | 440 | match 441 | \+|-|/|%|\*\*|\* 442 | name 443 | keyword.arithmetic.fsharp 444 | 445 | 446 | match 447 | (\|>|\|\?>|\->|\<\-|:>|:|\[|\]|\;|_|&&) 448 | name 449 | keyword.operator.name 450 | 451 | 452 | match 453 | \| 454 | name 455 | keyword.other.fsharp 456 | 457 | 458 | match 459 | =|>=|<=|<>|<|> 460 | name 461 | keyword.comparison.fsharp 462 | 463 | 464 | 465 | method_calls 466 | 467 | patterns 468 | 469 | 470 | applyEndPatternLast 471 | 1 472 | begin 473 | (?<!\w)([a-z]\w*)(\.) 474 | end 475 | (?=.) 476 | name 477 | meta.method-call.fsharp 478 | patterns 479 | 480 | 481 | captures 482 | 483 | 1 484 | 485 | name 486 | punctuation.separator.method-call.fsharp 487 | 488 | 489 | match 490 | [A-Z]\w*(\.) 491 | name 492 | meta.method.fsharp 493 | 494 | 495 | 496 | 497 | 498 | modules 499 | 500 | patterns 501 | 502 | 503 | begin 504 | \b(namespace|module)\s+([a-zA-Z][a-zA-Z0-9'_.]*) 505 | beginCaptures 506 | 507 | 1 508 | 509 | name 510 | keyword.other.fsharp 511 | 512 | 2 513 | 514 | name 515 | entity.name.section.fsharp 516 | 517 | 518 | end 519 | (\s|$) 520 | name 521 | meta.module.namespace.fsharp 522 | patterns 523 | 524 | 525 | captures 526 | 527 | 1 528 | 529 | name 530 | punctuation.separator.module-reference.fsharp 531 | 532 | 2 533 | 534 | name 535 | support.other.module.fsharp 536 | 537 | 538 | match 539 | (\.)([A-Z][a-zA-Z0-9'_]*) 540 | name 541 | support.other.module.fsharp 542 | 543 | 544 | 545 | 546 | begin 547 | \b(open)\s+([A-Z][a-zA-Z0-9'_]*)(?=(\.[A-Z][a-zA-Z0-9_]*)*) 548 | beginCaptures 549 | 550 | 1 551 | 552 | name 553 | keyword.other.fsharp 554 | 555 | 2 556 | 557 | name 558 | support.other.module.fsharp 559 | 560 | 561 | end 562 | (\s|$) 563 | name 564 | meta.module.open.fsharp 565 | patterns 566 | 567 | 568 | captures 569 | 570 | 1 571 | 572 | name 573 | punctuation.separator.module-reference.fsharp 574 | 575 | 2 576 | 577 | name 578 | support.other.module.fsharp 579 | 580 | 581 | match 582 | (\.)([A-Z][a-zA-Z0-9'_]*) 583 | name 584 | support.other.module.fsharp 585 | 586 | 587 | 588 | 589 | begin 590 | ^\s*(module)\s+([A-Z][a-zA-Z0-9'_]*)\s*(=)\s*([A-Z][a-zA-Z0-9'_]*) 591 | beginCaptures 592 | 593 | 1 594 | 595 | name 596 | keyword.other.module-definition.fsharp 597 | 598 | 2 599 | 600 | name 601 | entity.name.type.module.fsharp 602 | 603 | 3 604 | 605 | name 606 | punctuation.separator.module-definition.fsharp 607 | 608 | 4 609 | 610 | name 611 | support.other.module.fsharp 612 | 613 | 614 | end 615 | (\s|$) 616 | name 617 | meta.module.alias.fsharp 618 | patterns 619 | 620 | 621 | captures 622 | 623 | 1 624 | 625 | name 626 | punctuation.separator.module-reference.fsharp 627 | 628 | 2 629 | 630 | name 631 | support.other.module.fsharp 632 | 633 | 634 | match 635 | (\.)([A-Z][a-zA-Z0-9'_]*) 636 | name 637 | support.other.module.fsharp 638 | 639 | 640 | 641 | 642 | applyEndPatternLast 643 | 1 644 | begin 645 | (?<!\w)([A-Z][a-zA-Z0-9_]*)(\.) 646 | beginCaptures 647 | 648 | 1 649 | 650 | name 651 | support.other.module.fsharp 652 | 653 | 2 654 | 655 | name 656 | punctuation.separator.module-reference.fsharp 657 | 658 | 659 | end 660 | (?=.) 661 | name 662 | meta.module.reference.fsharp 663 | patterns 664 | 665 | 666 | captures 667 | 668 | 1 669 | 670 | name 671 | punctuation.separator.module-reference.fsharp 672 | 673 | 674 | match 675 | [A-Z][a-zA-Z0-9_]+(\.) 676 | name 677 | support.other.module.fsharp 678 | 679 | 680 | 681 | 682 | 683 | sequences 684 | 685 | patterns 686 | 687 | 688 | begin 689 | (seq)(?=\s+\{) 690 | beginCaptures 691 | 692 | 1 693 | 694 | name 695 | storage.type.fsharp 696 | 697 | 698 | end 699 | (\}) 700 | name 701 | meta.sequence.definition.fsharp 702 | patterns 703 | 704 | 705 | include 706 | $self 707 | 708 | 709 | 710 | 711 | 712 | string-escapes 713 | 714 | patterns 715 | 716 | 717 | match 718 | \\[bnrt\\"'] 719 | name 720 | constant.character.escape.fsharp 721 | 722 | 723 | match 724 | \\(u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8}) 725 | name 726 | constant.character.escape.unicode-sequence.fsharp 727 | 728 | 729 | match 730 | \\. 731 | name 732 | invalid.illegal.character.string.fsharp 733 | 734 | 735 | 736 | string-format-specs 737 | 738 | patterns 739 | 740 | 741 | match 742 | %[-\+0 #]?[bcsdiuxXoeEfFgGeEMOAat] 743 | name 744 | constant.character.format.specification.fsharp 745 | 746 | 747 | match 748 | (%\+A) 749 | name 750 | constant.character.format.specification.fsharp 751 | 752 | 753 | 754 | strings 755 | 756 | patterns 757 | 758 | 759 | begin 760 | (""") 761 | beginCaptures 762 | 763 | 1 764 | 765 | name 766 | punctuation.definition.string.begin.fsharp 767 | 768 | 769 | end 770 | (""") 771 | endCaptures 772 | 773 | 1 774 | 775 | name 776 | punctuation.definition.string.end.fsharp 777 | 778 | 779 | name 780 | string.quoted.triple.fsharp 781 | patterns 782 | 783 | 784 | match 785 | \\$[ \t]* 786 | name 787 | punctuation.separator.string.ignore-eol.fsharp 788 | 789 | 790 | match 791 | \\([\\'ntbr]|u[a-fA-F0-9]{4}|u[a-fA-F0-9]{8}) 792 | name 793 | constant.character.string.escape.fsharp 794 | 795 | 796 | match 797 | \\(?![\\'ntbr]|u[a-fA-F0-9]{4}|u[a-fA-F0-9]{8}). 798 | name 799 | invalid.illeagal.character.string.fsharp 800 | 801 | 802 | 803 | 804 | begin 805 | (@)(") 806 | beginCaptures 807 | 808 | 1 809 | 810 | name 811 | keyword.other.fsharp 812 | 813 | 2 814 | 815 | name 816 | punctuation.definition.string.begin.fsharp 817 | 818 | 819 | end 820 | (?<!")(")(?=[^"]) 821 | endCaptures 822 | 823 | 1 824 | 825 | name 826 | punctuation.definition.string.end.fsharp 827 | 828 | 829 | name 830 | string.quoted.double.verbatim.fsharp 831 | patterns 832 | 833 | 834 | match 835 | ("") 836 | name 837 | constant.character.escape.fsharp 838 | 839 | 840 | 841 | 842 | begin 843 | (?=[^\\])(") 844 | beginCaptures 845 | 846 | 1 847 | 848 | name 849 | punctuation.definition.string.begin.fsharp 850 | 851 | 852 | end 853 | (") 854 | endCaptures 855 | 856 | 1 857 | 858 | name 859 | punctuation.definition.string.end.fsharp 860 | 861 | 862 | name 863 | string.quoted.double.fsharp 864 | patterns 865 | 866 | 867 | include 868 | #string-escapes 869 | 870 | 871 | include 872 | #string-format-specs 873 | 874 | 875 | match 876 | \\$[ \t]* 877 | name 878 | punctuation.separator.string.ignore-eol.fsharp 879 | 880 | 881 | 882 | 883 | 884 | structure 885 | 886 | patterns 887 | 888 | 889 | begin 890 | \( 891 | end 892 | \) 893 | name 894 | meta.paren-group.fsharp 895 | patterns 896 | 897 | 898 | include 899 | $self 900 | 901 | 902 | 903 | 904 | 905 | type-abbreviation 906 | 907 | patterns 908 | 909 | 910 | begin 911 | \s*(type)\s+ 912 | beginCaptures 913 | 914 | 1 915 | 916 | name 917 | keyword.other.fsharp 918 | 919 | 920 | end 921 | $ 922 | patterns 923 | 924 | 925 | match 926 | =|-> 927 | name 928 | keyword.other.fsharp 929 | 930 | 931 | begin 932 | < 933 | end 934 | > 935 | name 936 | keyword.other.fsharp 937 | patterns 938 | 939 | 940 | match 941 | \'[a-zA-Z] 942 | name 943 | storage.type.generic.fsharp 944 | 945 | 946 | 947 | 948 | match 949 | \'[a-zA-Z] 950 | name 951 | storage.type.generic.fsharp 952 | 953 | 954 | 955 | 956 | 957 | type-generic 958 | 959 | patterns 960 | 961 | 962 | match 963 | \'[a-zA-Z](?!\') 964 | name 965 | storage.type.generic.fsharp 966 | 967 | 968 | 969 | variables 970 | 971 | patterns 972 | 973 | 974 | include 975 | #type-generic 976 | 977 | 978 | match 979 | \(\) 980 | name 981 | variable.parameter.unit.fsharp 982 | 983 | 984 | match 985 | (?<=:)\s*[a-zA-Z][\w.']* 986 | name 987 | storage.type.name.fsharp 988 | 989 | 990 | match 991 | [a-zA-Z]\w* 992 | name 993 | variable.parameter.fsharp 994 | 995 | 996 | 997 | 998 | scopeName 999 | source.fsharp 1000 | uuid 1001 | 6017A74A-C6EA-47A0-8DF4-E59C931316FA 1002 | 1003 | 1004 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | from logging.handlers import RotatingFileHandler 2 | from os import path 3 | import logging 4 | import logging.config 5 | import logging.handlers 6 | import os 7 | import sys 8 | 9 | 10 | class LogDir(object): 11 | ''' 12 | Locates the log dir for plugin logs. 13 | ''' 14 | 15 | @staticmethod 16 | def find(): 17 | return LogDir()._find_log_dir() 18 | 19 | def _test(self, a, b): 20 | if a == b: 21 | return 'folder' 22 | elif a == b + '.sublime-package': 23 | return 'sublime-package' 24 | 25 | def _find_path(self, start, package): 26 | while True: 27 | result = self._test(path.basename(start), package) 28 | 29 | if result == 'folder': 30 | if path.exists(path.join(path.dirname(start), 'User')): 31 | return path.join(path.dirname(start), '.logs') 32 | 33 | elif result == 'sublime-package': 34 | parent = path.dirname(start) 35 | if path.exists(path.join(path.dirname(parent), 'Packages')): 36 | return path.join(path.dirname(parent), 'Packages', '.logs') 37 | 38 | if path.dirname(start) == start: 39 | return 40 | 41 | start = path.dirname(start) 42 | 43 | def _find_log_dir(self): 44 | package = __name__.split('.')[0] 45 | 46 | if package == '__main__': 47 | return 48 | 49 | start = path.dirname(__file__) 50 | 51 | logs_path = self._find_path(start, package) 52 | 53 | if not logs_path: 54 | return 55 | 56 | if not path.exists(logs_path): 57 | os.mkdir(logs_path) 58 | return logs_path 59 | 60 | 61 | class NullPluginLogger(object): 62 | ''' 63 | Supresses log records. 64 | ''' 65 | 66 | def __init__(self, name): 67 | pass 68 | 69 | def debug(self, message, *args, **kwargs): 70 | pass 71 | 72 | def info(self, message, *args, **kwargs): 73 | pass 74 | 75 | def warn(self, message, *args, **kwargs): 76 | pass 77 | 78 | def warning(self, message, *args, **kwargs): 79 | pass 80 | 81 | def error(self, message, *args, **kwargs): 82 | pass 83 | 84 | def critical(self, message, *args, **kwargs): 85 | pass 86 | 87 | 88 | class PluginLogger(object): 89 | ''' 90 | Logs events. 91 | ''' 92 | 93 | log_dir = LogDir.find() 94 | 95 | def __init__(self, name): 96 | self.logger = logging.getLogger(name) 97 | default_level = logging.ERROR 98 | user_level = self._get_log_level_from_file() 99 | self.logger.setLevel(user_level if user_level is not None else default_level) 100 | 101 | f = logging.Formatter('%(asctime)s %(levelname)-5s %(name)s %(message)s') 102 | 103 | consoleHandler = logging.StreamHandler() 104 | consoleHandler.setLevel(logging.WARNING) 105 | consoleHandler.setFormatter(f) 106 | self.logger.addHandler(consoleHandler) 107 | 108 | file_name = self._file_name() 109 | if file_name: 110 | fileHandler = RotatingFileHandler(file_name, maxBytes=1<<10) 111 | fileHandler.setFormatter(f) 112 | self.logger.addHandler(fileHandler) 113 | else: 114 | print("Vintageous: cannot find log file path: %s" % file_name) 115 | 116 | def warn_aboug_logging_level(self): 117 | if self.logger.level <= logging.DEBUG: 118 | package = __name__.split('.')[0] 119 | self.warning("debug level set to DEBUG; check or delete %s", self._get_path_to_log()) 120 | 121 | def _get_path_to_log(self): 122 | package = __name__.split('.')[0] 123 | p = path.join(self.log_dir, package) 124 | return p 125 | 126 | def _get_log_level_from_file(self): 127 | p = self._get_path_to_log() 128 | if path.exists(p): 129 | with open(p, 'rt') as f: 130 | text = f.read().strip().upper() 131 | return getattr(logging, text, None) 132 | 133 | def _file_name(self): 134 | p = __name__.split('.')[0] 135 | return os.path.join(self.log_dir, '{}.log'.format(p)) 136 | 137 | def debug(self, message, *args, **kwargs): 138 | self.logger.debug(message, *args, **kwargs) 139 | 140 | def info(self, message, *args, **kwargs): 141 | self.logger.info(message, *args, **kwargs) 142 | 143 | def warn(self, message, *args, **kwargs): 144 | self.logger.warning(message, *args, **kwargs) 145 | 146 | def warning(self, message, *args, **kwargs): 147 | self.logger.warning(message, *args, **kwargs) 148 | 149 | def error(self, message, *args, **kwargs): 150 | self.logger.error(message, *args, **kwargs) 151 | 152 | def critical(self, message, *args, **kwargs): 153 | self.logger.critical(message, *args, **kwargs) 154 | 155 | 156 | PluginLogger(__name__).warn_aboug_logging_level() 157 | -------------------------------------------------------------------------------- /src/_init_.py: -------------------------------------------------------------------------------- 1 | # We don't use __init__.py because ST & Python load the file twice and two 2 | # instances of fsautocomplete are started. 3 | 4 | import logging 5 | 6 | from FSharp import PluginLogger 7 | from FSharp.lib.editor import Editor 8 | from FSharp.lib.response_processor import process_resp 9 | 10 | 11 | logger = PluginLogger(__name__) 12 | 13 | logger.debug('starting editor context...') 14 | 15 | editor_context = None 16 | editor_context = Editor(process_resp) 17 | 18 | 19 | def plugin_unloaded(): 20 | editor_context.fsac.stop() 21 | -------------------------------------------------------------------------------- /src/execute.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details. 2 | # All rights reserved. Use of this source code is governed by a BSD-style 3 | # license that can be found in the LICENSE file.) 4 | 5 | '''Our own ST `exec` command. 6 | 7 | Mostly lifted from Default.exec.py 8 | ''' 9 | 10 | import sublime 11 | import sublime_plugin 12 | 13 | import os 14 | import functools 15 | import time 16 | 17 | from Default.exec import ProcessListener 18 | from Default.exec import AsyncProcess 19 | from FSharp.subtrees.plugin_lib.sublime import after 20 | 21 | from FSharp.subtrees.plugin_lib.panels import OutputPanel 22 | 23 | 24 | class fs_exec(sublime_plugin.WindowCommand, ProcessListener): 25 | def run(self, 26 | cmd=None, 27 | shell_cmd=None, 28 | file_regex="", 29 | line_regex="", 30 | working_dir="", 31 | encoding="utf-8", 32 | env={}, 33 | quiet=False, 34 | kill=False, 35 | word_wrap=True, 36 | syntax="Packages/Text/Plain text.tmLanguage", 37 | preamble='', 38 | panel_name='fs.out', 39 | # Catches "path" and "shell" 40 | **kwargs): 41 | 42 | if kill: 43 | if hasattr(self, 'proc') and self.proc: 44 | self.proc.kill() 45 | self.proc = None 46 | self.append_string(None, "[Cancelled]") 47 | return 48 | 49 | # TODO(guillermooo): We cannot have multiple processes running at the 50 | # same time, or processes that use separate output panels. 51 | if not hasattr(self, 'out_panel'): 52 | # Try not to call get_output_panel until the regexes are assigned 53 | self.out_panel = OutputPanel(panel_name) 54 | 55 | # Default to the current files directory if no working directory was given 56 | if (not working_dir and 57 | self.window.active_view() and 58 | self.window.active_view().file_name()): 59 | working_dir = os.path.dirname( 60 | self.window.active_view().file_name()) 61 | 62 | self.out_panel.set("result_file_regex", file_regex) 63 | self.out_panel.set("result_line_regex", line_regex) 64 | self.out_panel.set("result_base_dir", working_dir) 65 | self.out_panel.set("word_wrap", word_wrap) 66 | self.out_panel.set("line_numbers", False) 67 | self.out_panel.set("gutter", False) 68 | self.out_panel.set("scroll_past_end", False) 69 | self.out_panel.view.assign_syntax(syntax) 70 | 71 | self.encoding = encoding 72 | self.quiet = quiet 73 | 74 | self.proc = None 75 | if not self.quiet: 76 | if shell_cmd: 77 | print("Running " + shell_cmd) 78 | else: 79 | print("Running " + " ".join(cmd)) 80 | sublime.status_message("Building") 81 | 82 | if preamble: 83 | self.append_string(self.proc, preamble) 84 | 85 | show_panel_on_build = sublime.load_settings( 86 | "Dart - Plugin Settings.sublime-settings").get("show_panel_on_build", True) 87 | if show_panel_on_build: 88 | self.out_panel.show() 89 | 90 | merged_env = env.copy() 91 | if self.window.active_view(): 92 | user_env = self.window.active_view().settings().get('build_env') 93 | if user_env: 94 | merged_env.update(user_env) 95 | 96 | # Change to the working dir, rather than spawning the process with it, 97 | # so that emitted working dir relative path names make sense 98 | if working_dir: 99 | os.chdir(working_dir) 100 | 101 | self.debug_text = "" 102 | if shell_cmd: 103 | self.debug_text += "[shell_cmd: " + shell_cmd + "]\n" 104 | else: 105 | self.debug_text += "[cmd: " + str(cmd) + "]\n" 106 | self.debug_text += "[dir: " + str(os.getcwd()) + "]\n" 107 | if "PATH" in merged_env: 108 | self.debug_text += "[path: " + str(merged_env["PATH"]) + "]" 109 | else: 110 | self.debug_text += "[path: " + str(os.environ["PATH"]) + "]" 111 | 112 | try: 113 | # Forward kwargs to AsyncProcess 114 | self.proc = AsyncProcess(cmd, shell_cmd, merged_env, self, 115 | **kwargs) 116 | except Exception as e: 117 | self.append_string(None, str(e) + "\n") 118 | self.append_string(None, self.debug_text + "\n") 119 | if not self.quiet: 120 | self.append_string(None, "[Finished]") 121 | 122 | def append_data(self, proc, data): 123 | if proc != self.proc: 124 | # a second call to exec has been made before the first one 125 | # finished, ignore it instead of intermingling the output. 126 | if proc: 127 | proc.kill() 128 | return 129 | 130 | try: 131 | str_ = data.decode(self.encoding) 132 | except UnicodeEncodeError: 133 | str_ = "[Decode error - output not " + self.encoding + "]\n" 134 | proc = None 135 | return 136 | 137 | # Normalize newlines, Sublime Text always uses a single \n separator 138 | # in memory. 139 | str_ = str_.replace('\r\n', '\n').replace('\r', '\n') 140 | 141 | self.out_panel.write(str_) 142 | 143 | def append_string(self, proc, str): 144 | self.append_data(proc, str.encode(self.encoding)) 145 | 146 | def finish(self, proc): 147 | if not self.quiet: 148 | elapsed = time.time() - proc.start_time 149 | exit_code = proc.exit_code() 150 | if (exit_code == 0) or (exit_code == None): 151 | self.append_string(proc, "[Finished in %.1fs]" % (elapsed)) 152 | else: 153 | self.append_string(proc, 154 | "[Finished in %.1fs with exit code %d]\n" % 155 | (elapsed, exit_code)) 156 | self.append_string(proc, self.debug_text) 157 | 158 | if proc != self.proc: 159 | return 160 | 161 | # XXX: What's this for? 162 | errs = self.out_panel.view.find_all_results() 163 | if len(errs) == 0: 164 | sublime.status_message("Build finished") 165 | else: 166 | sublime.status_message(("Build finished with %d errors") % len(errs)) 167 | 168 | def on_data(self, proc, data): 169 | after(0, functools.partial(self.append_data, proc, data)) 170 | 171 | def on_finished(self, proc): 172 | after(0, functools.partial(self.finish, proc)) 173 | -------------------------------------------------------------------------------- /src/fsac/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/zarchive-sublime-fsharp-package/e8f29405b5ad1d452bc556ab4742fc9d9e84776b/src/fsac/__init__.py -------------------------------------------------------------------------------- /src/fsac/client.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import queue 4 | import threading 5 | 6 | from .server import _internal_comm 7 | from .server import requests_queue 8 | from .server import responses_queue 9 | 10 | 11 | _logger = logging.getLogger(__name__) 12 | 13 | 14 | def read_responses(responses, messages, resp_proc): 15 | """Reads responses from server and forwards them to @resp_proc. 16 | """ 17 | while True: 18 | try: 19 | data = responses.get(block=True, timeout=5) 20 | if not data: 21 | _logger.warning('no response data to consume; exiting') 22 | break 23 | 24 | try: 25 | if messages.get(block=False) == STOP_SIGNAL: 26 | _logger.info('asked to stop; complying') 27 | break 28 | except: 29 | pass 30 | 31 | _logger.debug('response data read: %s', data) 32 | resp_proc(json.loads(data.decode('utf-8'))) 33 | except queue.Empty: 34 | pass 35 | 36 | _logger.info('stopped reading responses') 37 | 38 | 39 | class FsacClient(object): 40 | """Client for fsautocomplete.exe server. 41 | """ 42 | def __init__(self, server, resp_proc): 43 | self.requests = requests_queue 44 | self.server = server 45 | 46 | threading.Thread(target=read_responses, args=(responses_queue, 47 | _internal_comm, resp_proc)).start() 48 | 49 | def stop(self): 50 | self.server.stop() 51 | 52 | def send_request(self, request): 53 | _logger.debug('sending request: %s', str(request)[:100]) 54 | self.requests.put(request.encode()) 55 | -------------------------------------------------------------------------------- /src/fsac/fsac/KEEPME: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/zarchive-sublime-fsharp-package/e8f29405b5ad1d452bc556ab4742fc9d9e84776b/src/fsac/fsac/KEEPME -------------------------------------------------------------------------------- /src/fsac/pipe_server.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details. 2 | # All rights reserved. Use of this source code is governed by a BSD-style 3 | # license that can be found in the LICENSE file.) 4 | 5 | '''Wraps a process to make it act as a pipe server. Takes care of supressing 6 | console windows under Windows and other housekeeping. 7 | ''' 8 | 9 | from contextlib import contextmanager 10 | from subprocess import PIPE 11 | from subprocess import Popen 12 | import os 13 | import subprocess 14 | import threading 15 | 16 | 17 | @contextmanager 18 | def pushd(to): 19 | old = os.getcwd() 20 | try: 21 | os.chdir(to) 22 | # TODO(guillermooo): makes more sense to return 'old' 23 | yield to 24 | finally: 25 | os.chdir(old) 26 | 27 | 28 | def supress_window(): 29 | """Returns a STARTUPINFO structure configured to supress windows. 30 | Useful, for example, to supress console windows. 31 | 32 | Works only on Windows. 33 | """ 34 | if os.name == 'nt': 35 | startupinfo = subprocess.STARTUPINFO() 36 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 37 | startupinfo.wShowWindow = subprocess.SW_HIDE 38 | return startupinfo 39 | return None 40 | 41 | 42 | # _logger = PluginLogger(__name__) 43 | 44 | 45 | class PipeServer(object): 46 | '''Starts as process and communicates with it via pipes. 47 | ''' 48 | status_lock = threading.RLock() 49 | 50 | def __init__(self, args): 51 | self.proc = None 52 | self.args = args 53 | 54 | @property 55 | def is_running(self): 56 | '''Returns `True` if the server seems to be responsive. 57 | ''' 58 | try: 59 | with PipeServer.status_lock: 60 | return not self.proc.stdin.closed 61 | except AttributeError: 62 | # _logger.debug('PipeServer not started yet') 63 | return 64 | 65 | def start(self, working_dir='.'): 66 | with PipeServer.status_lock: 67 | if self.is_running: 68 | # _logger.debug( 69 | # 'tried to start an already running PipeServer; aborting') 70 | return 71 | 72 | with pushd(working_dir): 73 | # _logger.debug('starting PipeServer with args: %s', self.args) 74 | self.proc = Popen(self.args, 75 | stdout=PIPE, 76 | stdin=PIPE, 77 | stderr=PIPE, 78 | startupinfo=supress_window()) 79 | 80 | def stop(self): 81 | # _logger.debug('stopping PipeServer...') 82 | self.proc.stdin.close() 83 | self.proc.stdout.close() 84 | self.proc.kill() 85 | -------------------------------------------------------------------------------- /src/fsac/request.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details. 2 | # All rights reserved. Use of this source code is governed by a BSD-style 3 | # license that can be found in the LICENSE file.) 4 | 5 | 6 | class Request (object): 7 | def __init__(self, timeout=250, add_newline=True): 8 | self.add_newline = add_newline 9 | self.timeout = timeout 10 | 11 | def encode(self): 12 | data = str(self) 13 | if self.add_newline: 14 | data += '\n' 15 | return data.encode ('utf-8') 16 | 17 | 18 | class CompilerLocationRequest(Request): 19 | def __init__(self, *args, **kwargs): 20 | super ().__init__ (*args, **kwargs) 21 | 22 | def __str__(self): 23 | return 'compilerlocation' 24 | 25 | 26 | class ProjectRequest(Request): 27 | def __init__(self, project_file, *args, **kwargs): 28 | super ().__init__ (*args, **kwargs) 29 | self.project_file = project_file 30 | 31 | def __str__(self): 32 | return 'project "{0}"'.format(self.project_file) 33 | 34 | 35 | class ParseRequest(Request): 36 | def __init__(self, file_name, content='', full=True, *args, **kwargs): 37 | super ().__init__ (*args, add_newline=False, **kwargs) 38 | self.file_name = file_name 39 | self.content = content 40 | self.full = full 41 | 42 | def __str__(self): 43 | cmd = 'parse "{0}"'.format(self.file_name) 44 | if self.full: 45 | cmd += ' full' 46 | cmd += '\n' 47 | cmd += self.content + '\n<>\n' 48 | return cmd 49 | 50 | 51 | class FindDeclRequest(Request): 52 | def __init__(self, file_name, row, col, *args, **kwargs): 53 | super().__init__ (*args, **kwargs) 54 | self.file_name = file_name 55 | self.row = row 56 | self.col = col 57 | 58 | def __str__(self): 59 | return 'finddecl "{0}" {1} {2} {3}'.format( 60 | self.file_name, 61 | self.row, 62 | self.col, 63 | self.timeout, 64 | ) 65 | 66 | 67 | class CompletionRequest(Request): 68 | def __init__(self, file_name, row, col, *args, **kwargs): 69 | super().__init__ (*args, **kwargs) 70 | self.file_name = file_name 71 | self.row = row 72 | self.col = col 73 | 74 | def __str__(self): 75 | return 'completion "{0}" {1} {2} {3}'.format( 76 | self.file_name, 77 | self.row, 78 | self.col, 79 | self.timeout, 80 | ) 81 | 82 | 83 | class TooltipRequest(Request): 84 | def __init__(self, file_name, row, col, *args, **kwargs): 85 | super().__init__ (*args, **kwargs) 86 | self.file_name = file_name 87 | self.row = row 88 | self.col = col 89 | 90 | def __str__(self): 91 | return 'tooltip "{0}" {1} {2} {3}'.format( 92 | self.file_name, 93 | self.row, 94 | self.col, 95 | self.timeout, 96 | ) 97 | 98 | 99 | class DeclarationsRequest(Request): 100 | def __init__(self, file_name, *args, **kwargs): 101 | super ().__init__ (*args, **kwargs) 102 | self.file_name = file_name 103 | 104 | def __str__(self): 105 | return 'declarations "{0}"'.format(self.file_name) 106 | 107 | 108 | class DataRequest(Request): 109 | def __init__(self, *args, content='', add_newline=False, **kwargs): 110 | super ().__init__ (*args, add_newline=add_newline, **kwargs) 111 | self.content = content 112 | 113 | def __str__(self): 114 | return self.content 115 | 116 | 117 | class AdHocRequest (Request): 118 | def __init__(self, content, *args, **kwargs): 119 | super ().__init__ (*args, **kwargs) 120 | self.content = content 121 | 122 | def __str__(self): 123 | return self.content 124 | 125 | -------------------------------------------------------------------------------- /src/fsac/response.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details. 2 | # All rights reserved. Use of this source code is governed by a BSD-style 3 | # license that can be found in the LICENSE file.) 4 | 5 | import sublime 6 | 7 | 8 | class CompilerLocationResponse (object): 9 | def __init__(self, content): 10 | self.content = content 11 | 12 | @property 13 | def compilers_path(self): 14 | return self.content['Data'] 15 | 16 | 17 | class ProjectResponse (object): 18 | def __init__(self, content): 19 | self.content = content 20 | 21 | @property 22 | def files(self): 23 | return self.content['Data']['Files'] 24 | 25 | @property 26 | def framework(self): 27 | return self.content ['Data']['Framework'] 28 | 29 | @property 30 | def output(self): 31 | return self.content ['Data']['Output'] 32 | 33 | @property 34 | def output(self): 35 | return self.content ['Data']['References'] 36 | 37 | 38 | class TopLevelDeclaration(object): 39 | def __init__(self, data): 40 | self.data = data 41 | 42 | @property 43 | def first_location(self): 44 | col = self.data['BodyRange']['Item1']['Column'] 45 | row = self.data['BodyRange']['Item1']['Line'] 46 | return (row - 1, col) 47 | 48 | @property 49 | def name(self): 50 | return self.data['Name'] 51 | 52 | def __str__(self): 53 | return 'Declaration({0})<{1},{2}>'.format(self.name, *self.first_location) 54 | 55 | def to_menu_data(self): 56 | return [self.name, 'fs_go_to_location', {'loc': list(self.first_location)}] 57 | 58 | 59 | class DeclarationsResponse(object): 60 | def __init__(self, data): 61 | self.data = data 62 | 63 | @property 64 | def declarations(self): 65 | for decl in self.data['Data'][0]['Nested']: 66 | yield TopLevelDeclaration (decl) 67 | 68 | def __str__(self): 69 | return '' 70 | 71 | 72 | class ErrorInfo(object): 73 | def __init__(self, data): 74 | self.data = data 75 | 76 | @property 77 | def start_line(self): 78 | return self.data['StartLine'] 79 | 80 | @property 81 | def start_line_alternate(self): 82 | return self.data['StartLineAlternate'] 83 | 84 | @property 85 | def end_line(self): 86 | return self.data['EndLine'] 87 | 88 | @property 89 | def end_line_alternate(self): 90 | return self.data['EndLineAlternate'] 91 | 92 | @property 93 | def start_column(self): 94 | return self.data['StartColumn'] 95 | 96 | @property 97 | def end_column(self): 98 | return self.data['EndColumn'] 99 | 100 | @property 101 | def length(self): 102 | return self.end_column - self.start_column 103 | 104 | @property 105 | def severity(self): 106 | return self.data['Severity'] 107 | 108 | @property 109 | def message(self): 110 | return self.data['Message'] 111 | 112 | @property 113 | def subcategory(self): 114 | return self.data['Subcategory'] 115 | 116 | @property 117 | def file_name(self): 118 | return self.data['FileName'] 119 | 120 | def to_region(self, view): 121 | row = self.start_line 122 | col = self.start_column 123 | pt = view.text_point(row, col) 124 | return sublime.Region(pt, pt + self.length) 125 | 126 | def to_regex_result_data(self): 127 | d = { 128 | 'file_name': self.file_name, 129 | 'severity': self.severity.upper(), 130 | 'subcategory': self.subcategory.upper(), 131 | 'start_line': self.start_line_alternate, 132 | 'start_column': int(self.start_column) + 1, 133 | 'message': self.message 134 | } 135 | return d 136 | -------------------------------------------------------------------------------- /src/fsac/server.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import queue 5 | import threading 6 | 7 | from FSharp.subtrees.plugin_lib.plat import is_windows 8 | 9 | from .pipe_server import PipeServer 10 | 11 | 12 | PATH_TO_FSAC = os.path.join(os.path.dirname(__file__), 13 | 'fsac/fsautocomplete.exe') 14 | 15 | # Incoming requests from client (plugin). 16 | requests_queue = queue.Queue() 17 | # Outgoing responses from FsacServer. 18 | responses_queue = queue.Queue() 19 | # Special response queue for completions. 20 | # Completions don't ever hit the regular `responses_queue`. 21 | completions_queue = queue.Queue() 22 | # Internal queue to orchestrate thread termination, etc. 23 | _internal_comm = queue.Queue() 24 | 25 | STOP_SIGNAL = '__STOP' 26 | 27 | _logger = logging.getLogger(__name__) 28 | 29 | 30 | def request_reader(requests, server, internal_msgs=_internal_comm): 31 | '''Reads requests from @requests and forwards them to @server. 32 | 33 | @requests 34 | A queue of requests. 35 | @server 36 | `FsacServer` instance. 37 | @internal_msgs 38 | A queue of internal messages for orchestration. 39 | ''' 40 | while True: 41 | try: 42 | request = requests.get(block=True, timeout=5) 43 | 44 | try: 45 | # have we been asked to do anything? 46 | if internal_msgs.get(block=False) == STOP_SIGNAL: 47 | _logger.info('asked to exit; complying') 48 | internal_msgs.put(STOP_SIGNAL) 49 | break 50 | except queue.Empty: 51 | pass 52 | except Exception as e: 53 | _logger.error('unhandled exception: %s', e) 54 | # improve stack trace 55 | raise 56 | 57 | if not request: 58 | # Requests should always be valid, so log this but keep 59 | # running; most likely it isn't pathological. 60 | _logger.error('unexpected empty request: %s', request) 61 | continue 62 | 63 | _logger.debug('reading request: %s', request[:140]) 64 | server.fsac.proc.stdin.write(request) 65 | server.fsac.proc.stdin.flush() 66 | except queue.Empty: 67 | continue 68 | except Exception as e: 69 | _logger.error('unhandled exception: %s', e) 70 | raise 71 | 72 | _logger.info("request reader exiting...") 73 | 74 | 75 | def response_reader(responses, server, internal_msgs=_internal_comm): 76 | '''Reads requests from @server and forwards them to @responses. 77 | 78 | @responses 79 | A queue of responses. 80 | @server 81 | `PipeServer` instance wrapping `fsautocomplete.exe`. 82 | @internal_msgs 83 | A queue of internal messages for orchestration. 84 | ''' 85 | while True: 86 | try: 87 | data = server.fsac.proc.stdout.readline() 88 | if not data: 89 | _logger.debug('no data; exiting') 90 | break 91 | 92 | try: 93 | # Have we been asked to do anything? 94 | if internal_msgs.get(block=False) == STOP_SIGNAL: 95 | print('asked to exit; complying') 96 | internal_msgs.put(STOP_SIGNAL) 97 | break 98 | except queue.Empty: 99 | pass 100 | except Exception as e: 101 | _logger.error('unhandled exception: %s', e) 102 | raise 103 | 104 | _logger.debug('reading response: %s', data[:140]) 105 | # TODO: if we're decoding here, .put() the decoded data. 106 | data_json = json.loads(data.decode('utf-8')) 107 | if data_json['Kind'] == 'completion': 108 | completions_queue.put(data) 109 | continue 110 | 111 | responses.put(data) 112 | except queue.Empty: 113 | continue 114 | except Exception as e: 115 | _logger.error('unhandled exception: %s', e) 116 | raise 117 | 118 | _logger.info("response reader exiting...") 119 | 120 | 121 | class FsacServer(object): 122 | '''Wraps `fsautocomplete.exe`. 123 | ''' 124 | def __init__(self, cmd): 125 | ''' 126 | @cmd 127 | A command line as a list to start fsautocomplete.exe. 128 | ''' 129 | fsac = PipeServer(cmd) 130 | fsac.start() 131 | fsac.proc.stdin.write('outputmode json\n'.encode('ascii')) 132 | fsac.proc.stdin.flush() 133 | self.fsac = fsac 134 | 135 | threading.Thread( 136 | target=request_reader, args=(requests_queue, self)).start() 137 | threading.Thread( 138 | target=response_reader, args=(responses_queue, self)).start() 139 | 140 | def stop(self): 141 | self._internal_comm.put(STOP_SIGNAL) 142 | self.fsac.stop() 143 | 144 | 145 | def start(path=PATH_TO_FSAC): 146 | '''Starts a `FsacServer`. 147 | 148 | Returns a `PipeServer`. 149 | 150 | @path 151 | Path to `fsautocomplete.exe`. 152 | ''' 153 | args = [path] if is_windows() else ['mono', path] 154 | return FsacServer(args) 155 | -------------------------------------------------------------------------------- /src/fsharp.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details. 2 | # All rights reserved. Use of this source code is governed by a BSD-style 3 | # license that can be found in the LICENSE file.) 4 | 5 | import json 6 | import logging 7 | import os 8 | import queue 9 | 10 | import sublime 11 | import sublime_plugin 12 | 13 | from FSharp import PluginLogger 14 | from FSharp._init_ import editor_context 15 | from FSharp.fsac.request import AdHocRequest 16 | from FSharp.fsac.request import CompletionRequest 17 | from FSharp.fsac.request import DataRequest 18 | from FSharp.fsac.request import DeclarationsRequest 19 | from FSharp.fsac.request import FindDeclRequest 20 | from FSharp.fsac.request import ParseRequest 21 | from FSharp.fsac.request import ProjectRequest 22 | from FSharp.fsac.request import TooltipRequest 23 | from FSharp.fsac.response import CompilerLocationResponse 24 | from FSharp.fsac.response import DeclarationsResponse 25 | from FSharp.fsac.response import ErrorInfo 26 | from FSharp.fsac.response import ProjectResponse 27 | from FSharp.fsac.server import completions_queue 28 | from FSharp.lib.project import FileInfo 29 | from FSharp.lib.response_processor import add_listener 30 | from FSharp.lib.response_processor import ON_COMPLETIONS_REQUESTED 31 | from FSharp.lib.response_processor import process_resp 32 | from FSharp.lib.response_processor import raise_event 33 | from FSharp.subtrees.plugin_lib.context import ContextProviderMixin 34 | from FSharp.subtrees.plugin_lib.panels import OutputPanel 35 | 36 | 37 | _logger = PluginLogger(__name__) 38 | 39 | 40 | def erase_status(view, key): 41 | view.erase_status(key) 42 | 43 | 44 | class fs_run_fsac(sublime_plugin.WindowCommand): 45 | '''Runs an fsautocomplete.exe command. 46 | ''' 47 | def run(self, cmd): 48 | _logger.debug ('running fsac action: %s', cmd) 49 | if not cmd: 50 | return 51 | 52 | if cmd == 'project': 53 | self.do_project() 54 | return 55 | 56 | if cmd == 'parse': 57 | self.do_parse() 58 | return 59 | 60 | if cmd == 'declarations': 61 | self.do_declarations() 62 | return 63 | 64 | if cmd == 'compilerlocation': 65 | self.do_compiler_location() 66 | return 67 | 68 | if cmd == 'finddecl': 69 | self.do_find_decl() 70 | return 71 | 72 | if cmd == 'completion': 73 | self.do_completion() 74 | return 75 | 76 | if cmd == 'tooltip': 77 | self.do_tooltip() 78 | return 79 | 80 | if cmd == 'run-file': 81 | self.do_run_file() 82 | 83 | def get_active_file_name(self): 84 | try: 85 | fname = self.window.active_view().file_name() 86 | except AttributeError as e: 87 | return 88 | return fname 89 | 90 | def get_insertion_point(self): 91 | view = self.window.active_view() 92 | if not view: 93 | return None 94 | try: 95 | sel = view.sel()[0] 96 | except IndexError as e: 97 | return None 98 | return view.rowcol(sel.b) 99 | 100 | def do_project(self): 101 | fname = self.get_active_file_name () 102 | if not fname: 103 | return 104 | editor_context.fsac.send_request(ProjectRequest(fname)) 105 | 106 | def do_parse(self): 107 | fname = self.get_active_file_name() 108 | if not fname: 109 | return 110 | 111 | try: 112 | v = self.window.active_view() 113 | except AttributeError: 114 | return 115 | else: 116 | if not v: 117 | return 118 | content = v.substr(sublime.Region(0, v.size())) 119 | editor_context.fsac.send_request(ParseRequest(fname, content=content)) 120 | 121 | def do_declarations(self): 122 | fname = self.get_active_file_name() 123 | if not fname: 124 | return 125 | editor_context.fsac.send_request(DeclarationsRequest(fname)) 126 | 127 | def do_compiler_location(self): 128 | editor_context.fsac.send_request(CompilerLocationRequest()) 129 | 130 | def do_find_decl(self): 131 | fname = self.get_active_file_name() 132 | if not fname: 133 | return 134 | 135 | try: 136 | (row, col) = self.get_insertion_point() 137 | except TypeError as e: 138 | return 139 | else: 140 | self.do_parse() 141 | editor_context.fsac.send_request(FindDeclRequest(fname, row + 1, col + 1)) 142 | 143 | def do_completion(self): 144 | fname = self.get_active_file_name () 145 | if not fname: 146 | return 147 | 148 | try: 149 | (row, col) = self.get_insertion_point() 150 | except TypeError as e: 151 | return 152 | else: 153 | # raise first, because the event listener drains the completions queue 154 | raise_event(ON_COMPLETIONS_REQUESTED, {}) 155 | self.do_parse() 156 | editor_context.fsac.send_request(CompletionRequest(fname, row + 1, col + 1)) 157 | self.window.run_command('auto_complete') 158 | 159 | def do_tooltip(self): 160 | fname = self.get_active_file_name() 161 | if not fname: 162 | return 163 | 164 | try: 165 | (row, col) = self.get_insertion_point() 166 | except TypeError: 167 | return 168 | else: 169 | self.do_parse() 170 | editor_context.fsac.send_request(TooltipRequest(fname, row + 1, col + 1)) 171 | 172 | def do_run_file(self): 173 | try: 174 | fname = self.window.active_view().file_name() 175 | except AttributeError: 176 | return 177 | else: 178 | self.window.run_command('fs_run_interpreter', { 179 | 'fname': fname 180 | }) 181 | 182 | 183 | class fs_go_to_location (sublime_plugin.WindowCommand): 184 | def run(self, loc): 185 | v = self.window.active_view() 186 | pt = v.text_point(*loc) 187 | v.sel().clear() 188 | v.sel().add(sublime.Region(pt)) 189 | v.show_at_center(pt) 190 | 191 | 192 | class fs_show_menu(sublime_plugin.WindowCommand): 193 | '''Generic command to show a menu. 194 | ''' 195 | def run(self, items): 196 | ''' 197 | @items 198 | A list of items following this structure: 199 | item 0: name 200 | item 1: Sublime Text command name 201 | item 2: dictionary of arguments for the command 202 | ''' 203 | self.items = items 204 | self.names = names = [name for (name, _, _) in items] 205 | self.window.show_quick_panel(self.names, self.on_done) 206 | 207 | def on_done(self, idx): 208 | if idx == -1: 209 | return 210 | _, cmd, args = self.items[idx] 211 | if cmd: 212 | self.window.run_command (cmd, args or {}) 213 | 214 | 215 | class fs_show_data(sublime_plugin.WindowCommand): 216 | '''A simple command to use the quick panel as a data display. 217 | ''' 218 | def run(self, data): 219 | self.window.show_quick_panel(data, None, sublime.MONOSPACE_FONT) 220 | 221 | 222 | # TODO: move this to the command palette. 223 | class fs_show_options(sublime_plugin.WindowCommand): 224 | """Displays the main menu for F# commands. 225 | """ 226 | ITEMS = { 227 | 'F#: Show Declarations': 'declarations', 228 | 'F#: Show Tooltip': 'tooltip', 229 | 'F#: Run File': 'run-file', 230 | } 231 | 232 | def run(self): 233 | self.window.show_quick_panel( 234 | list(sorted(fs_show_options.ITEMS.keys())), 235 | self.on_done) 236 | 237 | def on_done(self, idx): 238 | if idx == -1: 239 | return 240 | key = list(sorted(fs_show_options.ITEMS.keys()))[idx] 241 | cmd = fs_show_options.ITEMS[key] 242 | self.window.run_command('fs_run_fsac', {'cmd': cmd}) 243 | 244 | 245 | class fs_run_interpreter(sublime_plugin.WindowCommand): 246 | def run(self, fname): 247 | assert fname, 'bad argument' 248 | 249 | f = FileInfo(fname) 250 | if not os.path.exists(f.path): 251 | _logger.debug('file must be saved first: %s', f.path) 252 | return 253 | 254 | if not f.is_fsharp_script_file: 255 | _logger.debug('not a script file: %s', f.path) 256 | return 257 | 258 | self.window.run_command('fs_exec', { 259 | 'shell_cmd': '"{}" "{}"'.format(editor_context.interpreter_path, f.path), 260 | 'working_dir': os.path.dirname(f.path) 261 | }) 262 | -------------------------------------------------------------------------------- /src/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/zarchive-sublime-fsharp-package/e8f29405b5ad1d452bc556ab4742fc9d9e84776b/src/lib/__init__.py -------------------------------------------------------------------------------- /src/lib/editor.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details. 2 | # All rights reserved. Use of this source code is governed by a BSD-style 3 | # license that can be found in the LICENSE file.) 4 | 5 | import os 6 | import logging 7 | import threading 8 | 9 | import sublime 10 | 11 | from FSharp.fsac import server 12 | from FSharp.fsac.client import FsacClient 13 | from FSharp.fsac.request import CompilerLocationRequest 14 | from FSharp.fsac.request import ParseRequest 15 | from FSharp.fsac.request import ProjectRequest 16 | from FSharp.fsac.response import ErrorInfo 17 | from FSharp.lib import response_processor 18 | from FSharp.lib.errors_panel import FSharpErrorsPanel 19 | from FSharp.lib.project import FileInfo 20 | from FSharp.lib.project import FSharpProjectFile 21 | from FSharp.lib.response_processor import ON_COMPILER_PATH_AVAILABLE 22 | from FSharp.lib.response_processor import ON_ERRORS_AVAILABLE 23 | 24 | 25 | _logger = logging.getLogger(__name__) 26 | 27 | 28 | class Editor(object): 29 | """Global editor state. 30 | """ 31 | def __init__(self, resp_proc): 32 | _logger.info ('Starting F# language services...') 33 | 34 | self.fsac = FsacClient(server.start(), resp_proc) 35 | 36 | self.compilers_path = None 37 | self.project_file = None 38 | 39 | self._errors = [] 40 | self.errors_panel = FSharpErrorsPanel() 41 | 42 | self.fsac.send_request(CompilerLocationRequest()) 43 | # todo: register as decorator instead? 44 | response_processor.add_listener(ON_COMPILER_PATH_AVAILABLE, 45 | self.on_compiler_path_available) 46 | 47 | response_processor.add_listener(ON_ERRORS_AVAILABLE, 48 | self.on_errors_available) 49 | 50 | self._write_lock = threading.Lock() 51 | 52 | def on_compiler_path_available(self, data): 53 | self.compilers_path = data['response'].compilers_path 54 | 55 | def on_errors_available(self, data): 56 | self.errors = data['response']['Data'] 57 | self.errors_panel.update((ErrorInfo(e) for e in self.errors), sort_key=lambda x: x.start_line) 58 | self.errors_panel.display() 59 | 60 | @property 61 | def errors(self): 62 | with self._write_lock: 63 | return self._errors 64 | 65 | @errors.setter 66 | def errors(self, value): 67 | assert isinstance(value, list), 'bad call' 68 | with self._write_lock: 69 | self._errors = value 70 | 71 | @property 72 | def compiler_path(self): 73 | if self.compilers_path is None: 74 | return 75 | return os.path.join(self.compilers_path, 'fsc.exe') 76 | 77 | @property 78 | def interpreter_path(self): 79 | if self.compilers_path is None: 80 | return None 81 | return os.path.join(self.compilers_path, 'fsi.exe') 82 | 83 | def update_project_data(self, fs_file): 84 | assert isinstance(fs_file, FileInfo), 'wrong argument: %s' % fs_file 85 | # todo: run in alternate thread 86 | 87 | # fsautocomplete.exe doesn't link F# script files to any .fsproj file, 88 | # so bail out. 89 | if fs_file.is_fsharp_script_file: 90 | return 91 | 92 | if not self.project_file or not self.project_file.governs(fs_file.path): 93 | self.project_file = FSharpProjectFile.from_path(fs_file.path) 94 | 95 | if not self.project_file: 96 | _logger.info('could not find a .fsproj file for %s' % fs_file) 97 | return 98 | 99 | # fsautocomplete.exe takes care of managing .fsproj files, so we 100 | # can add as many as we need. 101 | self.set_project() 102 | 103 | def set_project(self): 104 | self.fsac.send_request(ProjectRequest(self.project_file.path)) 105 | 106 | def parse_file(self, fs_file, content): 107 | self.fsac.send_request(ParseRequest(fs_file.path, content)) 108 | 109 | def parse_view(self, view, force=False): 110 | """ 111 | Sends a parse request to fsac. 112 | 113 | @view 114 | The view whose content should be parsed. 115 | 116 | @force 117 | If `True`, the @view will be parsed even if it's clean. 118 | """ 119 | 120 | if not (view.is_dirty() or force): 121 | return 122 | 123 | # FIXME: In ST, I think a file may have a .file_name() and still not 124 | # exist on disk because it's been unlinked. 125 | # ignore unsaved files 126 | fs_file = FileInfo(view) 127 | 128 | self.update_project_data(fs_file) 129 | # TODO: very inneficient? 130 | if fs_file.is_fsharp_code: 131 | content = view.substr(sublime.Region(0, view.size())) 132 | self.parse_file(fs_file, content) 133 | -------------------------------------------------------------------------------- /src/lib/errors_panel.py: -------------------------------------------------------------------------------- 1 | from FSharp.subtrees.plugin_lib.panels import ErrorsPanel 2 | 3 | 4 | class FSharpErrorsPanel(ErrorsPanel): 5 | """ 6 | Customized error panel for FSharp. 7 | """ 8 | 9 | def __init__(self): 10 | super().__init__(name='fsharp.errors') 11 | 12 | # Override defaults. 13 | self._sublime_syntax_file = 'Packages/FSharp/Support/FSharp Analyzer Output.sublime-syntax' 14 | self._tm_language_file = 'Packages/FSharp/Support/FSharp Analyzer Output.hidden-tmLanguage' 15 | self._errors_pattern = r'^\w+\|\w+\|(.+)\|(\d+)\|(\d+)\|(.+)$' 16 | self._errors_template = '{severity}|{subcategory}|{file_name}|{start_line}|{start_column}|{message}' 17 | 18 | def get_item_result_data(self, item): 19 | """ 20 | Subclasses must implement this method. 21 | 22 | Must return a dictionary to be used as data for `errors_template`. 23 | """ 24 | return item.to_regex_result_data() 25 | -------------------------------------------------------------------------------- /src/lib/project.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details. 2 | # All rights reserved. Use of this source code is governed by a BSD-style 3 | # license that can be found in the LICENSE file.) 4 | 5 | import os 6 | 7 | from FSharp.subtrees.plugin_lib import path 8 | from FSharp.subtrees.plugin_lib.path import find_file_by_extension 9 | 10 | 11 | def find_fsproject(start): 12 | """ 13 | Find a .fsproject file starting at @start path. 14 | 15 | Returns the path to the file or `None` if not found. 16 | """ 17 | 18 | return find_file_by_extension(start, 'fsproj') 19 | 20 | 21 | class FileInfo(path.FileInfo): 22 | """ 23 | Inspects a file for interesting properties from the plugin's POV. 24 | """ 25 | 26 | def __init__(self, *args, **kwargs): 27 | super().__init__(*args, **kwargs) 28 | 29 | @property 30 | def is_fsharp_file(self): 31 | return any((self.is_fsharp_code_file, 32 | self.is_fsharp_script_file, 33 | self.is_fsharp_project_file)) 34 | 35 | @property 36 | def is_fsharp_code(self): 37 | ''' 38 | Returns `True` if `self` is any sort of F# code file. 39 | ''' 40 | return (self.is_fsharp_code_file or self.is_fsharp_script_file) 41 | 42 | @property 43 | def is_fsharp_code_file(self): 44 | ''' 45 | Returns `True` if `self` is a .fs file. 46 | ''' 47 | return self.extension_equals('.fs') 48 | 49 | @property 50 | def is_fsharp_script_file(self): 51 | ''' 52 | Returns `True` if `self` is a .fsx/.fsscript file. 53 | ''' 54 | return self.extension_in('.fsx', '.fsscript') 55 | 56 | @property 57 | def is_fsharp_project_file(self): 58 | return self.extension_equals('.fsproj') 59 | 60 | 61 | class FSharpProjectFile(object): 62 | def __init__(self, path): 63 | assert path.endswith('.fsproj'), 'wrong fsproject path: %s' % path 64 | self.path = path 65 | self.parent = os.path.dirname(self.path) 66 | 67 | def __eq__(self, other): 68 | # todo: improve comparison 69 | return os.path.normpath(self.path) == os.path.normpath(other.path) 70 | 71 | def governs(self, fname): 72 | return fname.startswith(self.parent) 73 | 74 | @classmethod 75 | def from_path(cls, path): 76 | ''' 77 | @path 78 | A path to a file or directory. 79 | ''' 80 | fs_project = find_fsproject(path) 81 | if not fs_project: 82 | return None 83 | return FSharpProjectFile(fs_project) 84 | -------------------------------------------------------------------------------- /src/lib/response_processor.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details. 2 | # All rights reserved. Use of this source code is governed by a BSD-style 3 | # license that can be found in the LICENSE file.) 4 | 5 | import json 6 | import logging 7 | import os 8 | import queue 9 | 10 | import sublime 11 | import sublime_plugin 12 | 13 | from FSharp.lib.tooltips import show_info_tooltip 14 | from FSharp.fsac.response import CompilerLocationResponse 15 | from FSharp.fsac.response import DeclarationsResponse 16 | from FSharp.fsac.response import ErrorInfo 17 | from FSharp.fsac.response import ProjectResponse 18 | from FSharp.subtrees.plugin_lib.panels import OutputPanel 19 | 20 | 21 | _logger = logging.getLogger(__name__) 22 | 23 | 24 | ON_COMPILER_PATH_AVAILABLE = 'OnCompilerPathAvailableEvent' 25 | ON_COMPLETIONS_REQUESTED = 'OnCompletionsRequestedEvent' 26 | ON_ERRORS_AVAILABLE = 'OnErrorsAvailableEvent' 27 | 28 | _events = { 29 | ON_COMPILER_PATH_AVAILABLE: [], 30 | ON_COMPLETIONS_REQUESTED: [], 31 | ON_ERRORS_AVAILABLE: [], 32 | } 33 | 34 | 35 | def add_listener(event_name, f): 36 | '''Registers a listener for the @event_name event. 37 | ''' 38 | assert event_name, 'must provide "event_name" (actual: %s)' % event_name 39 | assert event_name in _events, 'unknown event name: %s' % event_name 40 | if f not in _events: 41 | _events[event_name].append(f) 42 | 43 | 44 | def raise_event(event_name=None, data={}): 45 | '''Raises an event. 46 | ''' 47 | assert event_name, 'must provide "event_name" (actual: %s)' % event_name 48 | assert event_name in _events, 'unknown event name: %s' % event_name 49 | assert isinstance(data, dict), '`data` must be a dict' 50 | for f in _events[event_name]: 51 | f(data) 52 | 53 | 54 | def process_resp(data): 55 | _logger.debug ('processing response data: %s', data) 56 | if data ['Kind'] == 'compilerlocation': 57 | r = CompilerLocationResponse (data) 58 | raise_event(ON_COMPILER_PATH_AVAILABLE, {'response': r}) 59 | return 60 | 61 | if data['Kind'] == 'project': 62 | r = ProjectResponse(data) 63 | _logger.debug('\n'.join(r.files)) 64 | return 65 | 66 | if data['Kind'] == 'errors': 67 | # todo: enable error navigation via standard keys 68 | try: 69 | v = sublime.active_window().active_view() 70 | except AttributeError: 71 | return 72 | 73 | if not v: 74 | return 75 | 76 | v.erase_regions('fs.errs') 77 | 78 | raise_event(ON_ERRORS_AVAILABLE, {'response': data}) 79 | if not data['Data']: 80 | return 81 | 82 | v.add_regions('fs.errs', 83 | [ErrorInfo(e).to_region(v) for e in data['Data']], 84 | 'invalid.illegal', 85 | 'dot', 86 | sublime.DRAW_SQUIGGLY_UNDERLINE | 87 | sublime.DRAW_NO_FILL | 88 | sublime.DRAW_NO_OUTLINE 89 | ) 90 | return 91 | 92 | if data['Kind'] == 'ERROR': 93 | _logger.error(str(data)) 94 | return 95 | 96 | if data['Kind'] == 'tooltip' and data['Data']: 97 | show_info_tooltip(data['Data']) 98 | return 99 | 100 | if data['Kind'] == 'INFO' and data['Data']: 101 | _logger.info(str(data)) 102 | return 103 | 104 | if data['Kind'] == 'finddecl' and data['Data']: 105 | fname = data['Data']['File'] 106 | row = data['Data']['Line'] 107 | col = data['Data']['Column'] + 1 108 | w = sublime.active_window() 109 | # todo: don't open file if we are looking at the requested file 110 | target = '{0}:{1}:{2}'.format(fname, row, col) 111 | w.open_file(target, sublime.ENCODED_POSITION) 112 | return 113 | 114 | if data['Kind'] == 'declarations' and data['Data']: 115 | decls = DeclarationsResponse(data) 116 | its = [decl.to_menu_data() for decl in decls.declarations] 117 | w = sublime.active_window() 118 | w.run_command ('fs_show_menu', {'items': its}) 119 | return 120 | 121 | if data['Kind'] == 'completion' and data['Data']: 122 | _logger.error('unexpected "completion" results - should be handled elsewhere') 123 | return 124 | -------------------------------------------------------------------------------- /src/lib/tooltips.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | from FSharp.subtrees.plugin_lib.sublime import after 4 | 5 | 6 | ERROR_TEMPLATE = """ 7 | 13 |
14 |  %(tag)s  %(message)s 15 |
16 | """ 17 | 18 | STATUS_TEMPLATE = """ 19 | 22 |
23 | %s 24 |
25 | """ 26 | 27 | TOOLTIP_ID = 0 28 | 29 | 30 | def next_id(): 31 | global TOOLTIP_ID 32 | while True: 33 | TOOLTIP_ID += 1 34 | yield TOOLTIP_ID 35 | if TOOLTIP_ID > 100: 36 | TOOLTIP_ID = 0 37 | 38 | 39 | id_generator = next_id() 40 | 41 | 42 | def show_status_tooltip(content, view=None, location=-1, timeout=0): 43 | content = STATUS_TEMPLATE % content 44 | show_tooltip(content, view, location, timeout) 45 | 46 | 47 | def show_info_tooltip(content, view=None, location=-1, timeout=0): 48 | content = ERROR_TEMPLATE % {'severity': 'INFO', 'tag': 'I', 'message': content} 49 | show_tooltip(content, view, location, timeout) 50 | 51 | 52 | def show_analysis_tooltip(content, view=None, location=-1, timeout=0): 53 | content['tag'] = content['severity'][0] 54 | show_tooltip(ERROR_TEMPLATE % content, view, location, timeout) 55 | 56 | 57 | def show_tooltip(content, view=None, location=-1, timeout=0): 58 | ''' 59 | Shows a tooltip. 60 | 61 | @content 62 | The tooltip's content (minihtml). 63 | 64 | @view 65 | The view in which the tooltip should be shown. If `None`, the active view 66 | will be used if available. 67 | 68 | @location 69 | Text location at which the tooltip will be shown. 70 | 71 | @timeout 72 | If greater than 0, the tooltip will be autohidden after @timeout 73 | milliseconds. 74 | ''' 75 | if not view: 76 | try: 77 | view = sublime.active_window().active_view() 78 | except AttributeError as e: 79 | return 80 | else: 81 | if not view: 82 | return 83 | 84 | view.show_popup(content, location=location, max_width=500) 85 | 86 | if timeout > 0: 87 | current_id = next(id_generator) 88 | after(timeout, lambda: _hide(view, current_id)) 89 | 90 | 91 | def _hide(view, target_tooltip_id): 92 | global TOOLTIP_ID 93 | if TOOLTIP_ID == target_tooltip_id: 94 | view.hide_popup() 95 | -------------------------------------------------------------------------------- /src/subtrees/plugin_lib/LICENSE.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/zarchive-sublime-fsharp-package/e8f29405b5ad1d452bc556ab4742fc9d9e84776b/src/subtrees/plugin_lib/LICENSE.txt -------------------------------------------------------------------------------- /src/subtrees/plugin_lib/README.md: -------------------------------------------------------------------------------- 1 | plugin_lib 2 | ========== 3 | 4 | A library of tools to create Sublime Text plugins 5 | -------------------------------------------------------------------------------- /src/subtrees/plugin_lib/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details. 2 | # All rights reserved. Use of this source code is governed by a BSD-style 3 | # license that can be found in the LICENSE file.) 4 | 5 | import logging 6 | import os 7 | 8 | import sublime 9 | 10 | 11 | class PluginLogger(object): 12 | """A logger intented to be used from plugin files inside this package. 13 | """ 14 | def __init__(self, name): 15 | logger = logging.getLogger(name) 16 | logger.setLevel(logging.ERROR) 17 | self.logger = logger 18 | 19 | def debug(self, msg, *args, **kwargs): 20 | self.logger.debug(msg, *args, **kwargs) 21 | 22 | def info(self, msg, *args, **kwargs): 23 | self.logger.info(msg, *args, **kwargs) 24 | 25 | def warning(self, msg, *args, **kwargs): 26 | self.logger.warning(msg, *args, **kwargs) 27 | 28 | def error(self, msg, *args, **kwargs): 29 | self.logger.error(msg, *args, **kwargs) 30 | 31 | def critical(self, msg, *args, **kwargs): 32 | self.logger.critical(msg, *args, **kwargs) 33 | -------------------------------------------------------------------------------- /src/subtrees/plugin_lib/collections.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details. 2 | # All rights reserved. Use of this source code is governed by a BSD-style 3 | # license that can be found in the LICENSE file.) 4 | 5 | 6 | class CircularArray(list): 7 | def __init__(self, *args, **kwargs): 8 | super().__init__(*args, **kwargs) 9 | self.index = None 10 | 11 | def forward(self): 12 | if self.index is None: 13 | self.index = 0 14 | return self[self.index] 15 | 16 | try: 17 | self.index += 1 18 | return self[self.index] 19 | except IndexError: 20 | self.index = 0 21 | return self[self.index] 22 | 23 | def backward(self): 24 | if self.index is None: 25 | self.index = -1 26 | return self[self.index] 27 | 28 | try: 29 | self.index -= 1 30 | return self[self.index] 31 | except IndexError: 32 | self.index = -1 33 | return self[self.index] 34 | -------------------------------------------------------------------------------- /src/subtrees/plugin_lib/context.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details. 2 | # All rights reserved. Use of this source code is governed by a BSD-style 3 | # license that can be found in the LICENSE file.) 4 | 5 | import sublime 6 | 7 | 8 | class ContextProviderMixin(object): 9 | '''Provides a method to evaluate contexts. 10 | 11 | Useful with sublime_plugin.EventListeners that need to evaluate contexts. 12 | ''' 13 | def _check(self, value, operator, operand, match_all): 14 | if operator == sublime.OP_EQUAL: 15 | if operand == True: 16 | return value 17 | elif operand == False: 18 | return not value 19 | elif operator == sublime.OP_NOT_EQUAL: 20 | if operand == True: 21 | return not value 22 | elif operand == False: 23 | return value 24 | -------------------------------------------------------------------------------- /src/subtrees/plugin_lib/events.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from collections import defaultdict 3 | 4 | import sublime_plugin 5 | 6 | from .sublime import after 7 | 8 | 9 | class IdleIntervalEventListener(sublime_plugin.EventListener): 10 | """ 11 | Base class. 12 | 13 | Monitors view idle time and calls .on_idle() after the specified duration. 14 | 15 | Idle time is defined as time during which no calls to .on_modified[_async]() 16 | have been made. 17 | 18 | Subclasses must implement .on_idle(view) and, if necessary, .check(view). 19 | 20 | We don't provide a default implementation of .on_idle(view). 21 | """ 22 | 23 | def __init__(self, *args, duration=500, **kwargs): 24 | """ 25 | @duration 26 | Interval after which an .on_idle() call will be made, expressed in 27 | milliseconds. 28 | """ 29 | 30 | # TODO: Maybe it's more efficient to collect .on_idle() functions and 31 | # manage edits globally, then call collected functions when idle. 32 | self.edits = defaultdict(int) 33 | self.lock = threading.Lock() 34 | 35 | # Expressed in milliseconds. 36 | self.duration = duration 37 | super().__init__(*args, **kwargs) 38 | 39 | @property 40 | def _is_subclass(self): 41 | return hasattr(self, 'on_idle') 42 | 43 | def _add_edit(self, view): 44 | with self.lock: 45 | self.edits[view.id()] += 1 46 | # TODO: are we running async or sync? 47 | after(self.duration, lambda: self._subtract_edit(view)) 48 | 49 | def _subtract_edit(self, view): 50 | with self.lock: 51 | self.edits[view.id()] -= 1 52 | if self.edits[view.id()] == 0: 53 | self.on_idle(view) 54 | 55 | def on_modified_async(self, view): 56 | # TODO: improve check for widgets and overlays. 57 | if not all((view, self._is_subclass, self.check(view))): 58 | return 59 | self._add_edit(view) 60 | 61 | # Override in derived class if needed. 62 | def check(self, view): 63 | """ 64 | Returs `True` if @view should be monitored for idleness. 65 | 66 | @view 67 | The view that is about to be monitored for idleness. 68 | """ 69 | return True 70 | -------------------------------------------------------------------------------- /src/subtrees/plugin_lib/filter.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details. 2 | # All rights reserved. Use of this source code is governed by a BSD-style 3 | # license that can be found in the LICENSE file.) 4 | 5 | from subprocess import Popen 6 | from subprocess import PIPE 7 | from subprocess import TimeoutExpired 8 | import threading 9 | 10 | from . import PluginLogger 11 | from .plat import supress_window 12 | from .text import clean 13 | from .text import decode 14 | 15 | 16 | _logger = PluginLogger(__name__) 17 | 18 | 19 | class TextFilter(object): 20 | '''Filters text through an external program (sync). 21 | ''' 22 | def __init__(self, args, timeout=10): 23 | self.args = args 24 | self.timeout = timeout 25 | # Encoding the external program likes to receive. 26 | self.in_encoding = 'utf-8' 27 | # Encoding the external program will emit. 28 | self.out_encoding = 'utf-8' 29 | 30 | self._proc = None 31 | 32 | def encode(self, text): 33 | return text.encode(self.in_encoding) 34 | 35 | def _start(self): 36 | try: 37 | self._proc = Popen(self.args, 38 | stdout=PIPE, 39 | stderr=PIPE, 40 | stdin=PIPE, 41 | startupinfo=supress_window()) 42 | except OSError as e: 43 | _logger.error('while starting text filter program: %s', e) 44 | return 45 | 46 | def filter(self, input_text): 47 | self._start() 48 | try: 49 | in_bytes = self.encode(input_text) 50 | out_bytes, err_bytes = self._proc.communicate(in_bytes, 51 | self.timeout) 52 | if err_bytes: 53 | _logger.error('while filtering text: %s', 54 | clean(decode(err_bytes, self.out_encoding))) 55 | return 56 | 57 | return clean(decode(out_bytes, self.out_encoding)) 58 | 59 | except TimeoutExpired: 60 | _logger.debug('text filter program response timed out') 61 | return 62 | 63 | except Exception as e: 64 | _logger.error('while running TextFilter: %s', e) 65 | return 66 | -------------------------------------------------------------------------------- /src/subtrees/plugin_lib/fs_completion.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details. 2 | # All rights reserved. Use of this source code is governed by a BSD-style 3 | # license that can be found in the LICENSE file.) 4 | 5 | 6 | # TODO(guillermooo): get fsystem items async 7 | # TODO(guillermooo): set limits to the no. of returned items 8 | # TODO(guillermooo): handle OS errors like permissions, etc. 9 | # TODO(guillermooo): performance: maybe store items in a tree, 10 | # skip list or some sort of indexed structure that improves recall time, 11 | # like indexing by prefix: 12 | # a, b, c, d, e, f, g ... ah, bh, ch, dh 13 | 14 | 15 | from collections import Counter 16 | import os 17 | import glob 18 | 19 | 20 | class CompletionsList(object): 21 | def __init__(self, items): 22 | self.items = items 23 | 24 | def __iter__(self): 25 | yield from self.items 26 | 27 | # TODO(guillermooo): move casesensitive to __init__ 28 | def iter_prefixed(self, prefix, casesensitive=False): 29 | if casesensitive: 30 | yield from (item for item in self 31 | if item.startswith(prefix)) 32 | else: 33 | yield from (item for item in self 34 | if item.lower().startswith(prefix.lower())) 35 | 36 | 37 | class FileSystemCompletion(object): 38 | def __init__(self, casesensitive=False): 39 | self.cached_items = None 40 | # path as provided by user 41 | self.user_path = None 42 | # TODO(guillermooo): set automatically based on OS 43 | self._casesensitive = casesensitive 44 | 45 | def do_refresh(self, new_path, force_refresh): 46 | seps_new = Counter(new_path)["/"] 47 | seps_old = Counter(self.user_path)["/"] 48 | 49 | # we've never tried to get completions yet, so try now 50 | if self.cached_items is None: 51 | self.user_path = os.path.abspath('.') 52 | return True 53 | 54 | # if we have 2 or more additional slashes, we can be sure the user 55 | # wants to drill down to a different directory. 56 | # if we had only 1 additional slash, it may indicate a directory, but 57 | # not necessarily any user-driven intention of drilling down in the 58 | # dir hierarchy. This is because we return items with slashes to 59 | # indicate directories. 60 | # 61 | # If we have fewer slashes in the new path, the user has modified it. 62 | if 0 > (seps_new - seps_old) > 1 or (seps_new - seps_old) < 0: 63 | return True 64 | 65 | return force_refresh 66 | 67 | def get_completions(self, path, force_refresh=False): 68 | # we are cycling through items in the same directory as last time, 69 | # so reuse the cached items 70 | if not self.do_refresh(path, force_refresh): 71 | cl = CompletionsList(self.cached_items) 72 | leaf = os.path.split(path)[1] 73 | return list(cl.iter_prefixed( 74 | leaf, 75 | casesensitive=self._casesensitive) 76 | ) 77 | 78 | # we need to refresh the cache, as we are in a different directory 79 | # now or we've been asked to nevertheless. 80 | self.user_path = self.unescape(path) 81 | abs_path = os.path.abspath(os.path.dirname(self.user_path)) 82 | leaf = os.path.split(self.user_path)[1] 83 | 84 | fs_items = glob.glob(self.user_path + '*') 85 | fs_items = self.process_items(fs_items) 86 | 87 | cl = CompletionsList(fs_items) 88 | self.cached_items = list(cl) 89 | 90 | return list(cl.iter_prefixed(leaf, 91 | casesensitive=self._casesensitive) 92 | ) 93 | 94 | def process_items(self, items): 95 | processed = [] 96 | for it in items: 97 | if not os.path.isdir(it): 98 | continue 99 | leaf = os.path.split(it)[1] 100 | leaf += '/' 101 | processed.append(self.escape(leaf)) 102 | return processed 103 | 104 | @classmethod 105 | def escape(cls, name): 106 | return name.replace(' ', '\\ ') 107 | 108 | @classmethod 109 | def unescape(cls, name): 110 | return name.replace('\\ ', ' ') 111 | -------------------------------------------------------------------------------- /src/subtrees/plugin_lib/io.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details. 2 | # All rights reserved. Use of this source code is governed by a BSD-style 3 | # license that can be found in the LICENSE file.) 4 | 5 | import threading 6 | 7 | import os 8 | 9 | 10 | class AsyncStreamReader(threading.Thread): 11 | '''Reads a process stream from an alternate thread. 12 | ''' 13 | def __init__(self, stream, on_data, *args, **kwargs): 14 | ''' 15 | @stream 16 | Stream to read from. 17 | 18 | @on_data 19 | Callback to call with bytes read from @stream. 20 | ''' 21 | super().__init__(*args, **kwargs) 22 | self.stream = stream 23 | self.on_data = on_data 24 | assert self.on_data, 'wrong call: must provide callback' 25 | 26 | def run(self): 27 | while True: 28 | data = self.stream.readline() 29 | if not data: 30 | return 31 | 32 | self.on_data(data) 33 | 34 | 35 | def touch(path): 36 | with open(path, 'wb') as f: 37 | f.close() 38 | -------------------------------------------------------------------------------- /src/subtrees/plugin_lib/panels.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details. 2 | # All rights reserved. Use of this source code is governed by a BSD-style 3 | # license that can be found in the LICENSE file.) 4 | 5 | from threading import Lock 6 | import os 7 | 8 | import sublime 9 | 10 | from .sublime import after 11 | 12 | 13 | class OutputPanel(object): 14 | _write_lock = Lock() 15 | 16 | """Manages an ST output panel. 17 | 18 | Can be used as a file-like object. 19 | """ 20 | 21 | def __init__(self, name, 22 | base_dir=None, 23 | syntax='Packages/Text/Plain text.tmLanguage', 24 | **kwargs): 25 | """ 26 | @name 27 | This panel's name. 28 | @base_dir 29 | Directory used to look files matched by regular expressions. 30 | @syntax: 31 | This panel's syntax. 32 | @kwargs 33 | Any number of settings to set in the underlying view via `.set()`. 34 | 35 | Common settings: 36 | - result_file_regex 37 | - result_line_regex 38 | - word_wrap 39 | - line_numbers 40 | - gutter 41 | - scroll_past_end 42 | """ 43 | 44 | self.name = name 45 | self.window = sublime.active_window() 46 | 47 | if not hasattr(self, 'view'): 48 | # Try not to call get_output_panel until the regexes are assigned 49 | self.view = self.window.create_output_panel(self.name) 50 | 51 | # Default to the current file directory 52 | if (not base_dir and 53 | self.window.active_view() and 54 | self.window.active_view().file_name()): 55 | base_dir = os.path.dirname(self.window.active_view().file_name()) 56 | 57 | self.set('result_base_dir', base_dir) 58 | self.set('syntax', syntax) 59 | 60 | self.set('result_file_regex', '') 61 | self.set('result_line_regex', '') 62 | self.set('word_wrap', False) 63 | self.set('line_numbers', False) 64 | self.set('gutter', False) 65 | self.set('scroll_past_end', False) 66 | 67 | def set(self, name, value): 68 | self.view.settings().set(name, value) 69 | 70 | def _clean_text(self, text): 71 | return text.replace('\r', '') 72 | 73 | def write(self, text): 74 | assert isinstance(text, str), 'must pass decoded text data' 75 | with OutputPanel._write_lock: 76 | do_write = lambda: self.view.run_command('append', { 77 | 'characters': self._clean_text(text), 78 | 'force': True, 79 | 'scroll_to_end': True, 80 | }) 81 | # XXX: If we don't sync with the GUI thread here, the command above 82 | # won't work if this method is called from .set_timeout_async(). 83 | # BUG? 84 | after(0, do_write) 85 | 86 | def flush(self): 87 | pass 88 | 89 | def hide(self): 90 | self.window.run_command('hide_panel', { 91 | 'panel': 'output.' + self.name}) 92 | 93 | def show(self): 94 | # Call create_output_panel a second time after assigning the above 95 | # settings, so that it'll be picked up as a result buffer 96 | self.window.create_output_panel(self.name) 97 | self.window.run_command('show_panel', { 98 | 'panel': 'output.' + self.name}) 99 | 100 | def close(self): 101 | pass 102 | 103 | 104 | # TOOD: fix this 105 | class ErrorPanel(object): 106 | def __init__(self): 107 | self.panel = OutputPanel('dart.info') 108 | self.panel.write('=' * 80) 109 | self.panel.write('\n') 110 | self.panel.write("Dart - Something's not quite right\n") 111 | self.panel.write('=' * 80) 112 | self.panel.write('\n') 113 | self.panel.write('\n') 114 | 115 | def write(self, text): 116 | self.panel.write(text) 117 | 118 | def show(self): 119 | self.panel.show() 120 | 121 | 122 | # TODO: move this to common plugin lib. 123 | class ErrorsPanel(object): 124 | """ 125 | A panel that displays errors and enables error navigation. 126 | """ 127 | _sublime_syntax_file = None 128 | _tm_language_file = None 129 | _errors_pattern = '' 130 | _errors_template = '' 131 | 132 | _lock = Lock() 133 | 134 | def __init__(self, name): 135 | """ 136 | @name 137 | The name of the underlying output panel. 138 | """ 139 | self.name = name 140 | self._errors = [] 141 | 142 | @property 143 | def errors(self): 144 | with self._lock: 145 | return self._errors 146 | 147 | @errors.setter 148 | def errors(self, value): 149 | with self._lock: 150 | self._errors = value 151 | 152 | @property 153 | def errors_pattern(self): 154 | """ 155 | Subclasses can override this to provide a more suitable pattern to 156 | capture errors. 157 | """ 158 | return self._errors_pattern 159 | 160 | @property 161 | def errors_template(self): 162 | """ 163 | Subclasses can override this to provide a more suitable template to 164 | display errors. 165 | """ 166 | return self._errors_template 167 | 168 | def display(self): 169 | if len(self.errors) == 0: 170 | panel = OutputPanel(self.name) 171 | panel.hide() 172 | return 173 | 174 | # Like this to avoid deadlock. XXX: Maybe use RLock instead? 175 | formatted = self.format() 176 | with self._lock: 177 | # XXX: If we store this panel as an instance member, it won't work. 178 | # Revise implementation. 179 | panel = OutputPanel(self.name) 180 | panel.set('result_file_regex', self.errors_pattern) 181 | # TODO: remove this when we don't support tmLanguage any more. 182 | if sublime.version() > '3083': 183 | panel.view.set_syntax_file(self._sublime_syntax_file) 184 | else: 185 | panel.view.set_syntax_file(self._tm_language_file) 186 | panel.write(formatted) 187 | # TODO(guillermooo): Do not show now if other panel is showing; 188 | # for example, the console. 189 | panel.show() 190 | 191 | def clear(self): 192 | self.errors = [] 193 | 194 | def update(self, errors, sort_key=None): 195 | self.errors = list(sorted(errors, key=sort_key)) 196 | 197 | def get_item_result_data(self, item): 198 | """ 199 | Subclasses must implement this method. 200 | 201 | Must return a dictionary to be used as data for `errors_template`. 202 | """ 203 | return {} 204 | 205 | def format(self): 206 | formatted = (self.errors_template.format(**self.get_item_result_data(e)) 207 | for e in self.errors) 208 | return '\n'.join(formatted) 209 | -------------------------------------------------------------------------------- /src/subtrees/plugin_lib/path.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details. 2 | # All rights reserved. Use of this source code is governed by a BSD-style 3 | # license that can be found in the LICENSE file.) 4 | 5 | '''Helper functions for path management. 6 | ''' 7 | 8 | import glob 9 | import os 10 | from os.path import join 11 | from contextlib import contextmanager 12 | 13 | import sublime 14 | 15 | from .plat import is_windows 16 | 17 | 18 | class FileInfo(object): 19 | """ 20 | Base class. 21 | 22 | Subclasses inspect a file for interesting properties from a plugin's POV. 23 | """ 24 | 25 | def __init__(self, view_or_fname): 26 | """ 27 | @view_or_fname 28 | A Sublime Text view or a file name. 29 | """ 30 | assert view_or_fname, 'wrong arg: %s' % view_or_fname 31 | self.view_or_fname = view_or_fname 32 | 33 | def __str__(self): 34 | return self.path 35 | 36 | @property 37 | def path(self): 38 | try: 39 | # The returned path can be None, for example, if the view is unsaved. 40 | return self.view_or_fname.file_name() 41 | except AttributeError: 42 | return self.view_or_fname 43 | 44 | def extension_equals(self, extension): 45 | return self.path and extension_equals(self.path, extension) 46 | 47 | def extension_in(self, *extensions): 48 | return self.path and any(self.extension_equals(ext) for ext in extensions) 49 | 50 | 51 | def extension_equals(path_or_view, extension): 52 | """Compares @path_or_view's extensions with @extension. 53 | 54 | Returns `True` if they are the same, `False` otherwise. 55 | Returns `False` if @path_or_view is a view and isn't saved on disk. 56 | """ 57 | try: 58 | if path_or_view.file_name() is None: 59 | return False 60 | return extension_equals(path_or_view.file_name(), extension) 61 | except AttributeError: 62 | try: 63 | return os.path.splitext(path_or_view)[1] == extension 64 | except Exception: 65 | raise TypeError('string or view required, got {}' 66 | .format(type(path_or_view))) 67 | 68 | 69 | def find_in_path(name, win_ext=''): 70 | '''Searches PATH for @name. 71 | 72 | Returns the path containing @name or `None` if not found. 73 | 74 | @name 75 | Binary to search for. 76 | 77 | @win_ext 78 | An extension that will be added to @name on Windows. 79 | ''' 80 | bin_name = join_on_win(name, win_ext) 81 | for path in os.environ['PATH'].split(os.path.pathsep): 82 | path = os.path.expandvars(os.path.expanduser(path)) 83 | if os.path.exists(os.path.join(path, bin_name)): 84 | return os.path.realpath(path) 85 | 86 | 87 | def find_file_by_extension(start, extension): 88 | ''' 89 | Finds a file in a directory hierarchy starting from @start and 90 | walking upwards. 91 | 92 | @start 93 | The directory to start from. 94 | 95 | @extension 96 | Sought extension. 97 | ''' 98 | if not os.path.exists(start): 99 | return 100 | 101 | pattern = os.path.join(start, "*." + extension) 102 | file_name = glob.glob(pattern) 103 | if file_name: 104 | return file_name[0] 105 | 106 | if os.path.dirname(start) == start: 107 | return 108 | 109 | return find_file_by_extension(os.path.dirname(start), extension) 110 | 111 | 112 | def find_file(start, fname): 113 | '''Finds a file in a directory hierarchy starting from @start and 114 | walking backwards. 115 | 116 | @start 117 | The directory to start from. 118 | 119 | @fname 120 | Sought file. 121 | ''' 122 | if not os.path.exists(start): 123 | return 124 | 125 | if os.path.exists(os.path.join(start, fname)): 126 | return os.path.join(start, fname) 127 | 128 | if os.path.dirname(start) == start: 129 | return 130 | 131 | return find_file(os.path.dirname(start), fname) 132 | 133 | 134 | def is_prefix(prefix, path): 135 | prefix = os.path.realpath(prefix) 136 | path = os.path.realpath(path) 137 | return path.startswith(prefix) 138 | 139 | 140 | def to_platform_path(original, append): 141 | """ 142 | Useful to add .exe to @original, .bat, etc if ST is running on Windows. 143 | 144 | @original 145 | Original path. 146 | @append 147 | Fragment to append to @original on Windows. 148 | """ 149 | if is_windows(): 150 | if append.startswith('.'): 151 | return original + append 152 | return join(original, append) 153 | return original 154 | 155 | 156 | def is_active_path(path): 157 | """Returns `True` if the current view's path equals @path. 158 | """ 159 | view = sublime.active_window().active_view() 160 | if not view: 161 | return 162 | return os.path.realpath(view.file_name()) == os.path.realpath(path) 163 | 164 | 165 | def is_active(view): 166 | """Returns `True` if @view is the view being currently edited. 167 | """ 168 | active_view = sublime.active_window().active_view() 169 | if not active_view: 170 | return 171 | return active_view == view 172 | 173 | 174 | @contextmanager 175 | def pushd(to): 176 | old = os.getcwd() 177 | try: 178 | os.chdir(to) 179 | # TODO(guillermooo): makes more sense to return 'old' 180 | yield to 181 | finally: 182 | os.chdir(old) 183 | 184 | 185 | def join_on_win(original, append): 186 | """ Useful to add .exe, .bat, etc. to @original if ST is running on 187 | Windows. 188 | 189 | @original 190 | Original path. 191 | 192 | @append 193 | Fragment to append to @original on Windows. If it's an extension 194 | (the fragment begins with '.'), it's tucked at the end of @original. 195 | Otherwise, it's joined as a path. 196 | """ 197 | if is_windows(): 198 | if append.startswith('.'): 199 | return original + append 200 | return os.path.join(original, append) 201 | return original 202 | -------------------------------------------------------------------------------- /src/subtrees/plugin_lib/plat.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details. 2 | # All rights reserved. Use of this source code is governed by a BSD-style 3 | # license that can be found in the LICENSE file.) 4 | 5 | '''Helper functions related to platform-specific issues. 6 | ''' 7 | 8 | import sublime 9 | 10 | from os.path import join 11 | import subprocess 12 | 13 | 14 | def is_windows(): 15 | """Returns `True` if ST is running on Windows. 16 | """ 17 | return sublime.platform() == 'windows' 18 | 19 | 20 | def supress_window(): 21 | """Returns a STARTUPINFO structure configured to supress windows. 22 | Useful, for example, to supress console windows. 23 | 24 | Works only on Windows. 25 | """ 26 | if is_windows(): 27 | startupinfo = subprocess.STARTUPINFO() 28 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 29 | startupinfo.wShowWindow = subprocess.SW_HIDE 30 | return startupinfo 31 | return None 32 | -------------------------------------------------------------------------------- /src/subtrees/plugin_lib/settings.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | 4 | # todo: move these tests to sublime_plugin_lib 5 | class FlexibleSetting(object): 6 | ''' 7 | Base class. 8 | 9 | Data descriptor that encapsulates access to a Sublime Text setting that 10 | can take any of the following forms: 11 | 12 | Scalar value 13 | ====================================================================== 14 | "color": "blue" 15 | ---------------------------------------------------------------------- 16 | Dictionary keyed by platform 17 | ====================================================================== 18 | "color" : { 19 | "windows": "blue", 20 | "linux": "orange", 21 | "osx": "green" 22 | } 23 | ---------------------------------------------------------------------- 24 | 25 | This way, users can specify the same setting globally as a scalar value, or 26 | more granularly, by platform, and the plugin code can read it in the same 27 | way in both cases. 28 | 29 | For example: 30 | 31 | class SomeSettingsClass: 32 | path_to_thing = FlexibleSettingSubclass(name='path_to_thing') 33 | 34 | settings = SomeSettingsClass() 35 | value = settings.path_to_thing 36 | 37 | Optionally, basic validation is configurable: 38 | 39 | class SomeSettingsClass: 40 | path_to_thing = FlexibleSettingSubclass(name='path_to_thing', expected_type=str) 41 | 42 | settings = SomeSettingsClass() 43 | value = settings.path_to_thing 44 | 45 | Validation errors raise a ValueError. 46 | 47 | Subclasses must at a minimum implement the .get() method. 48 | ''' 49 | 50 | def __init__(self, name, expected_type=None, default=None): 51 | self.name = name 52 | self.expected_type = expected_type 53 | self.default = default 54 | 55 | def __get__(self, obj, typ): 56 | if obj is None: 57 | return self 58 | 59 | scalar_or_dict = self.get(self.name) 60 | 61 | value = None 62 | try: 63 | value = scalar_or_dict[sublime.platform()] 64 | except TypeError: 65 | value = scalar_or_dict 66 | except KeyError: 67 | raise ValueError("no platform settings found for '%s' (%s)" % (self.name, sublime.platform())) 68 | 69 | value = value if (value is not None) else self.default 70 | 71 | value = self.validate(value) 72 | value = self.post_validate(value) 73 | return value 74 | 75 | def __set__(self, obj, val): 76 | raise NotImplementedException("can't do this now") 77 | 78 | def validate(self, value): 79 | if self.expected_type is None: 80 | return value 81 | assert isinstance(value, self.expected_type), 'validation failed for "%s". Got %s, expected %s' % (self.name, type(value), self.expected_type) 82 | return value 83 | 84 | def post_validate(self, value): 85 | ''' 86 | Subclasses should override this method if they need to do any 87 | postprocessing after the the value has been gotten and validated. 88 | 89 | Returns a settings' value. 90 | ''' 91 | return value 92 | 93 | def get(self, name): 94 | ''' 95 | Abstract method. 96 | 97 | Subclasses must implement here access to self.name's setting top-level 98 | value (a scalar value or a dictionary). 99 | ''' 100 | raise NotImplementedException('implement me') 101 | -------------------------------------------------------------------------------- /src/subtrees/plugin_lib/sublime.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details. 2 | # All rights reserved. Use of this source code is governed by a BSD-style 3 | # license that can be found in the LICENSE file.) 4 | 5 | '''Utilities based on the Sublime Text api. 6 | ''' 7 | import sublime 8 | 9 | 10 | # TODO(guillermooo): make an *_async version too? 11 | def after(timeout, f, *args, **kwargs): 12 | '''Runs @f after @timeout delay in milliseconds. 13 | 14 | @timeout 15 | Delay in milliseconds. 16 | 17 | @f 18 | Function to run passing it @*args and @*kwargs. 19 | ''' 20 | sublime.set_timeout(lambda: f(*args, **kwargs), timeout) 21 | -------------------------------------------------------------------------------- /src/subtrees/plugin_lib/subprocess.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details. 2 | # All rights reserved. Use of this source code is governed by a BSD-style 3 | # license that can be found in the LICENSE file.) 4 | 5 | from subprocess import Popen 6 | import os 7 | 8 | from . import PluginLogger 9 | from .plat import supress_window 10 | 11 | 12 | _logger = PluginLogger(__name__) 13 | 14 | 15 | def killwin32(proc): 16 | try: 17 | path = os.path.expandvars("%WINDIR%\\System32\\taskkill.exe") 18 | GenericBinary(show_window=False).start([path, "/pid", str(proc.pid)]) 19 | except Exception as e: 20 | _logger.error(e) 21 | 22 | 23 | class GenericBinary(object): 24 | '''Starts a process. 25 | ''' 26 | def __init__(self, *args, show_window=True): 27 | ''' 28 | @show_window 29 | Windows only. Whether to show a window. 30 | ''' 31 | self.args = args 32 | self.startupinfo = None 33 | if not show_window: 34 | self.startupinfo = supress_window() 35 | 36 | def start(self, args=[], env=None, shell=False, cwd=None): 37 | cmd = self.args + tuple(args) 38 | _logger.debug('running cmd line (GenericBinary): %s', cmd) 39 | Popen(cmd, startupinfo=self.startupinfo, env=env, shell=shell, 40 | cwd=cwd) 41 | -------------------------------------------------------------------------------- /src/subtrees/plugin_lib/text.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details. 2 | # All rights reserved. Use of this source code is governed by a BSD-style 3 | # license that can be found in the LICENSE file.) 4 | 5 | 6 | def decode_and_clean(data_bytes, encoding='utf-8'): 7 | return clean(decode(data_bytes, encoding)) 8 | 9 | 10 | def decode(data_bytes, encoding='utf-8'): 11 | return data_bytes.decode(encoding) 12 | 13 | 14 | def clean(text): 15 | return text.replace('\r', '') 16 | -------------------------------------------------------------------------------- /src/test_runner.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details. 2 | # All rights reserved. Use of this source code is governed by a BSD-style 3 | # license that can be found in the LICENSE file.) 4 | 5 | import sublime 6 | import sublime_plugin 7 | 8 | import os 9 | import unittest 10 | import contextlib 11 | import threading 12 | 13 | 14 | from FSharp.subtrees.plugin_lib.panels import OutputPanel 15 | 16 | 17 | class RunFsharpTests(sublime_plugin.WindowCommand): 18 | '''Runs tests and displays the result. 19 | 20 | - Do not use ST while tests are running. 21 | 22 | @working_dir 23 | Required. Should be the parent of the top-level directory for `tests`. 24 | 25 | @loader_pattern 26 | Optional. Only run tests matching this glob. 27 | 28 | @active_file_only 29 | Optional. Only run tests in the active file in ST. Shadows 30 | @loader_pattern. 31 | 32 | To use this runner conveniently, open the command palette and select one 33 | of the `Build: Dart - Test *` commands. 34 | ''' 35 | @contextlib.contextmanager 36 | def chdir(self, path=None): 37 | old_path = os.getcwd() 38 | if path is not None: 39 | assert os.path.exists(path), "'path' is invalid {}".format(path) 40 | os.chdir(path) 41 | yield 42 | if path is not None: 43 | os.chdir(old_path) 44 | 45 | def run(self, **kwargs): 46 | with self.chdir(kwargs.get('working_dir')): 47 | p = os.path.join(os.getcwd(), 'tests') 48 | patt = kwargs.get('loader_pattern', 'test*.py',) 49 | # TODO(guillermooo): I can't get $file to expand in the build 50 | # system. It should be possible to make the following code simpler 51 | # with it. 52 | if kwargs.get('active_file_only') is True: 53 | patt = os.path.basename(self.window.active_view().file_name()) 54 | suite = unittest.TestLoader().discover(p, pattern=patt) 55 | 56 | file_regex = r'^\s*File\s*"([^.].*?)",\s*line\s*(\d+),.*$' 57 | display = OutputPanel('fs.tests', file_regex=file_regex) 58 | display.show() 59 | runner = unittest.TextTestRunner(stream=display, verbosity=1) 60 | 61 | def run_and_display(): 62 | runner.run(suite) 63 | 64 | threading.Thread(target=run_and_display).start() 65 | -------------------------------------------------------------------------------- /src/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/zarchive-sublime-fsharp-package/e8f29405b5ad1d452bc556ab4742fc9d9e84776b/src/tests/__init__.py -------------------------------------------------------------------------------- /src/tests/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/zarchive-sublime-fsharp-package/e8f29405b5ad1d452bc556ab4742fc9d9e84776b/src/tests/lib/__init__.py -------------------------------------------------------------------------------- /src/tests/lib/test_editor.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/zarchive-sublime-fsharp-package/e8f29405b5ad1d452bc556ab4742fc9d9e84776b/src/tests/lib/test_editor.py -------------------------------------------------------------------------------- /src/tests/lib/test_events.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from FSharp.subtrees.plugin_lib.events import IdleIntervalEventListener 4 | 5 | 6 | class Test_IdleIntervalEventListener(unittest.TestCase): 7 | def testDefaultIdleInterval(self): 8 | listener = IdleIntervalEventListener() 9 | self.assertEqual(500, listener.duration) 10 | 11 | def testDoesNotImplement_on_idle(self): 12 | listener = IdleIntervalEventListener() 13 | self.assertFalse(hasattr(listener, 'on_idle')) 14 | 15 | def test_check_ReturnsTrueByDefault(self): 16 | listener = IdleIntervalEventListener() 17 | self.assertTrue(listener.check(view=None)) 18 | -------------------------------------------------------------------------------- /src/tests/lib/test_project.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import glob 3 | import os 4 | import tempfile 5 | import time 6 | import unittest 7 | 8 | import sublime 9 | 10 | from FSharp.lib.project import find_fsproject 11 | from FSharp.lib.project import FileInfo 12 | from FSharp.lib.project import FSharpProjectFile 13 | from FSharp.subtrees.plugin_lib.io import touch 14 | 15 | 16 | @contextlib.contextmanager 17 | def make_directories(dirs): 18 | tmp_dir = tempfile.TemporaryDirectory() 19 | current = tmp_dir.name 20 | for dd in dirs: 21 | for d in dd: 22 | current = os.path.join(current, d) 23 | os.mkdir(current) 24 | current = tmp_dir.name 25 | yield tmp_dir.name 26 | tmp_dir.cleanup() 27 | 28 | 29 | class Test_find_fsproject(unittest.TestCase): 30 | def testCanFind(self): 31 | with make_directories([["foo", "bar", "baz"]]) as tmp_root: 32 | fs_proj_file = os.path.join(tmp_root, 'hey.fsproj') 33 | touch(fs_proj_file) 34 | found = find_fsproject (os.path.join(tmp_root, 'foo/bar/baz')) 35 | self.assertEquals(found, fs_proj_file) 36 | 37 | 38 | class Test_FSharpProjectFile (unittest.TestCase): 39 | def testCanCreateFromPath(self): 40 | with tempfile.TemporaryDirectory() as tmp: 41 | f = os.path.join (tmp, 'foo.fsproj') 42 | touch (f) 43 | fs_project = FSharpProjectFile.from_path(f) 44 | self.assertEquals(fs_project.path, f) 45 | 46 | def testCanReturnParent(self): 47 | with tempfile.TemporaryDirectory() as tmp: 48 | f = os.path.join (tmp, 'foo.fsproj') 49 | touch (f) 50 | fs_project = FSharpProjectFile.from_path(f) 51 | self.assertEquals(fs_project.parent, tmp) 52 | 53 | def testCanBeCompared(self): 54 | with tempfile.TemporaryDirectory() as tmp: 55 | f = os.path.join (tmp, 'foo.fsproj') 56 | touch (f) 57 | fs_project_1 = FSharpProjectFile.from_path(f) 58 | fs_project_2 = FSharpProjectFile.from_path(f) 59 | self.assertEquals(fs_project_1, fs_project_2) 60 | 61 | def test_governs_SameLevel(self): 62 | with tempfile.TemporaryDirectory() as tmp: 63 | f = os.path.join (tmp, 'foo.fsproj') 64 | f2 = os.path.join (tmp, 'foo.fs') 65 | touch (f) 66 | touch (f2) 67 | fs_proj = FSharpProjectFile.from_path(f) 68 | self.assertTrue(fs_proj.governs (f2)) 69 | 70 | 71 | class Test_FSharpFile(unittest.TestCase): 72 | def setUp(self): 73 | self.win = sublime.active_window() 74 | 75 | def tearDown(self): 76 | self.win.run_command('close') 77 | 78 | def testCannotDetectCodeFileBasedOnlyOnSyntaxDef(self): 79 | v = self.win.new_file() 80 | v.set_syntax_file('Packages/FSharp/FSharp.tmLanguage') 81 | file_info = FileInfo(v) 82 | self.assertFalse(file_info.is_fsharp_code) 83 | 84 | def testCanDetectCodeFile(self): 85 | with tempfile.TemporaryDirectory() as tmp: 86 | f = os.path.join (tmp, 'foo.fs') 87 | touch (f) 88 | v = self.win.open_file(f) 89 | time.sleep(0.01) 90 | file_info = FileInfo(v) 91 | self.assertTrue (file_info.is_fsharp_code_file) 92 | 93 | def testCanDetectScriptFile(self): 94 | with tempfile.TemporaryDirectory() as tmp: 95 | f = os.path.join (tmp, 'foo.fsx') 96 | touch (f) 97 | v = self.win.open_file(f) 98 | time.sleep(0.01) 99 | file_info = FileInfo(v) 100 | self.assertTrue (file_info.is_fsharp_script_file) 101 | 102 | def testCanDetectCodeForCodeFile(self): 103 | with tempfile.TemporaryDirectory() as tmp: 104 | f = os.path.join (tmp, 'foo.fs') 105 | touch (f) 106 | v = self.win.open_file(f) 107 | time.sleep(0.01) 108 | file_info = FileInfo(v) 109 | self.assertTrue (file_info.is_fsharp_code) 110 | 111 | def testCanDetectCodeForScriptFile(self): 112 | with tempfile.TemporaryDirectory() as tmp: 113 | f = os.path.join (tmp, 'foo.fsx') 114 | touch (f) 115 | v = self.win.open_file(f) 116 | time.sleep(0.01) 117 | file_info = FileInfo(v) 118 | self.assertTrue (file_info.is_fsharp_code) 119 | 120 | def testCanDetectProjectFile(self): 121 | with tempfile.TemporaryDirectory() as tmp: 122 | f = os.path.join (tmp, 'foo.fsproj') 123 | touch (f) 124 | v = self.win.open_file(f) 125 | time.sleep(0.01) 126 | file_info = FileInfo (v) 127 | self.assertTrue (file_info.is_fsharp_project_file) 128 | 129 | 130 | class Test_FSharpFile_path(unittest.TestCase): 131 | def setUp(self): 132 | self.win = sublime.active_window() 133 | 134 | def tearDown(self): 135 | self.win.run_command('close') 136 | 137 | def testCannotFindPathIfNone(self): 138 | v = self.win.new_file() 139 | v.set_syntax_file('Packages/FSharp/FSharp.tmLanguage') 140 | file_info = FileInfo(v) 141 | self.assertEquals(file_info.path, None) 142 | -------------------------------------------------------------------------------- /src/xevents.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details. 2 | # All rights reserved. Use of this source code is governed by a BSD-style 3 | # license that can be found in the LICENSE file.) 4 | 5 | from collections import defaultdict 6 | import json 7 | import logging 8 | import threading 9 | 10 | import sublime 11 | import sublime_plugin 12 | 13 | from FSharp import PluginLogger 14 | from FSharp.fsac.server import completions_queue 15 | from FSharp.fsharp import editor_context 16 | from FSharp.lib.project import FileInfo 17 | from FSharp.lib.response_processor import add_listener 18 | from FSharp.lib.response_processor import ON_COMPLETIONS_REQUESTED 19 | from FSharp.subtrees.plugin_lib.context import ContextProviderMixin 20 | from FSharp.subtrees.plugin_lib.sublime import after 21 | from FSharp.subtrees.plugin_lib.events import IdleIntervalEventListener 22 | 23 | 24 | _logger = PluginLogger(__name__) 25 | 26 | 27 | class IdleParser(IdleIntervalEventListener): 28 | """ 29 | Reparses the current view after @self.duration milliseconds of inactivity. 30 | """ 31 | 32 | def __init__(self, *args, **kwargs): 33 | super().__init__(*args, **kwargs) 34 | self.duration = 1000 35 | 36 | def check(self, view): 37 | return FileInfo(view).is_fsharp_code 38 | 39 | def on_idle(self, view): 40 | editor_context.parse_view(view) 41 | 42 | 43 | class IdleAutocomplete(IdleIntervalEventListener): 44 | """ 45 | Shows the autocomplete list after @self.duration milliseconds of inactivity. 46 | """ 47 | 48 | def __init__(self, *args, **kwargs): 49 | super().__init__(*args, **kwargs) 50 | self.duration = 400 51 | 52 | # FIXME: we should exclude widgets and overlays in the base class. 53 | def check(self, view): 54 | # Offer F# completions in F# files when the caret isn't in a string or 55 | # comment. If strings or comments, offer plain Sublime Text completions. 56 | return (not self._in_string_or_comment(view) 57 | and FileInfo(view).is_fsharp_code) 58 | 59 | def on_idle(self, view): 60 | self._show_completions(view) 61 | 62 | def _show_completions(self, view): 63 | try: 64 | # TODO: We probably should show completions after other chars. 65 | is_after_dot = view.substr(view.sel()[0].b - 1) == '.' 66 | except IndexError: 67 | return 68 | 69 | if is_after_dot: 70 | view.window().run_command('fs_run_fsac', {'cmd': 'completion'}) 71 | 72 | def _in_string_or_comment(self, view): 73 | try: 74 | return view.match_selector(view.sel()[0].b, 75 | 'source.fsharp string, source.fsharp comment') 76 | except IndexError: 77 | pass 78 | 79 | 80 | class FSharpProjectTracker(sublime_plugin.EventListener): 81 | """ 82 | Event listeners. 83 | """ 84 | 85 | parsed = {} 86 | parsed_lock = threading.Lock() 87 | 88 | def on_activated_async(self, view): 89 | # It seems we may receive a None in some cases -- check for it. 90 | if not view or not view.file_name() or not FileInfo(view).is_fsharp_code: 91 | return 92 | 93 | with FSharpProjectTracker.parsed_lock: 94 | view_id = view.file_name() or view.id() 95 | if FSharpProjectTracker.parsed.get(view_id): 96 | return 97 | 98 | editor_context.parse_view(view, force=True) 99 | self.set_parsed(view, True) 100 | 101 | def on_load_async(self, view): 102 | self.on_activated_async(view) 103 | 104 | def set_parsed(self, view, value): 105 | with FSharpProjectTracker.parsed_lock: 106 | view_id = view.file_name() or view.id() 107 | FSharpProjectTracker.parsed[view_id] = value 108 | 109 | def on_modified_async(self, view): 110 | if not view or not view.file_name() or not FileInfo(view).is_fsharp_code: 111 | return 112 | 113 | self.set_parsed(view, False) 114 | 115 | 116 | class FSharpContextProvider(sublime_plugin.EventListener, ContextProviderMixin): 117 | """ 118 | Implements contexts for .sublime-keymap files. 119 | """ 120 | 121 | def on_query_context(self, view, key, operator, operand, match_all): 122 | if key == 'fs_is_code_file': 123 | value = FileInfo(view).is_fsharp_code 124 | return self._check(value, operator, operand, match_all) 125 | 126 | 127 | class FSharpAutocomplete(sublime_plugin.EventListener): 128 | """ 129 | Provides completion suggestions from fsautocomplete. 130 | """ 131 | 132 | WAIT_ON_COMPLETIONS = False 133 | _INHIBIT_OTHER = (sublime.INHIBIT_WORD_COMPLETIONS | 134 | sublime.INHIBIT_EXPLICIT_COMPLETIONS) 135 | 136 | @staticmethod 137 | def on_completions_requested(data): 138 | FSharpAutocomplete.WAIT_ON_COMPLETIONS = True 139 | 140 | @staticmethod 141 | def fetch_completions(): 142 | data = completions_queue.get(block=True, timeout=.75) 143 | data = json.loads(data.decode('utf-8')) 144 | completions = [[item["Name"], item["Name"]] for item in data['Data']] 145 | return completions 146 | 147 | @staticmethod 148 | def _in_string_or_comment(view, locations): 149 | return all((view.match_selector(loc, 'source.fsharp comment, source.fsharp string') 150 | or view.match_selector(loc - 1, 'source.fsharp comment, sorce.fsharp string')) 151 | for loc in locations) 152 | 153 | def on_query_completions(self, view, prefix, locations): 154 | if not FSharpAutocomplete.WAIT_ON_COMPLETIONS: 155 | if not FileInfo(view).is_fsharp_code: 156 | return [] 157 | 158 | if self._in_string_or_comment(view, locations): 159 | return [] 160 | 161 | return ([], self._INHIBIT_OTHER) 162 | 163 | try: 164 | return (self.fetch_completions(), self._INHIBIT_OTHER) 165 | # FIXME: Be more explicit about caught exceptions. 166 | except: 167 | return ([], self._INHIBIT_OTHER) 168 | finally: 169 | FSharpAutocomplete.WAIT_ON_COMPLETIONS = False 170 | 171 | 172 | # TODO: make decorator? 173 | add_listener(ON_COMPLETIONS_REQUESTED, FSharpAutocomplete.on_completions_requested) 174 | --------------------------------------------------------------------------------