├── .github └── workflows │ ├── formatting.yml │ └── main.yml ├── .gitignore ├── .haxerc ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── LICENSE.md ├── README.md ├── cases ├── documentSymbols │ ├── Expected.json │ └── Input.hx ├── foldingRange │ ├── Expected.json │ └── Input.hx └── hxformat.json ├── haxe_libraries ├── formatter.hxml ├── haxe-hxparser.hxml ├── haxeparser.hxml ├── hxjsonast.hxml ├── hxnodejs.hxml ├── hxparse.hxml ├── json2object.hxml ├── language-server-protocol.hxml ├── rename.hxml ├── safety.hxml ├── test-adapter.hxml ├── tokentree.hxml ├── uglifyjs.hxml ├── utest.hxml ├── vscode-json-rpc.hxml └── vshaxe-build.hxml ├── package-lock.json ├── package.json ├── shared └── haxeLanguageServer │ ├── DisplayServerConfig.hx │ ├── LanguageServerMethods.hx │ └── ServerRecordingEntryKind.hx ├── src └── haxeLanguageServer │ ├── Configuration.hx │ ├── Context.hx │ ├── Init.hx │ ├── Main.hx │ ├── documents │ ├── HaxeDocument.hx │ ├── HxTextDocument.hx │ ├── HxmlDocument.hx │ └── TextDocuments.hx │ ├── extensions │ ├── ArrayExtensions.hx │ ├── DocumentUriExtensions.hx │ ├── FsPathExtensions.hx │ ├── FunctionFormattingConfigExtensions.hx │ ├── PositionExtensions.hx │ ├── RangeExtensions.hx │ ├── ResponseErrorExtensions.hx │ └── StringExtensions.hx │ ├── features │ ├── CompletionFeature.hx │ ├── HoverFeature.hx │ ├── haxe │ │ ├── CodeLensFeature.hx │ │ ├── ColorProviderFeature.hx │ │ ├── DeterminePackageFeature.hx │ │ ├── DiagnosticsFeature.hx │ │ ├── DocumentFormattingFeature.hx │ │ ├── FindReferencesFeature.hx │ │ ├── GotoDefinitionFeature.hx │ │ ├── GotoImplementationFeature.hx │ │ ├── GotoTypeDefinitionFeature.hx │ │ ├── HoverFeature.hx │ │ ├── InlayHintFeature.hx │ │ ├── RenameFeature.hx │ │ ├── SignatureHelpFeature.hx │ │ ├── WorkspaceSymbolsFeature.hx │ │ ├── codeAction │ │ │ ├── CodeActionFeature.hx │ │ │ ├── DiagnosticsCodeActionFeature.hx │ │ │ ├── ExtractConstantFeature.hx │ │ │ ├── ExtractFunctionFeature.hx │ │ │ ├── ExtractTypeFeature.hx │ │ │ ├── OrganizeImportsFeature.hx │ │ │ └── diagnostics │ │ │ │ ├── CompilerErrorActions.hx │ │ │ │ ├── MissingArgumentsAction.hx │ │ │ │ ├── MissingFieldsActions.hx │ │ │ │ ├── OrganizeImportActions.hx │ │ │ │ ├── ParserErrorActions.hx │ │ │ │ ├── RemovableCodeActions.hx │ │ │ │ ├── UnresolvedIdentifierActions.hx │ │ │ │ ├── UnusedImportActions.hx │ │ │ │ └── import.hx │ │ ├── completion │ │ │ ├── CompletionContextData.hx │ │ │ ├── CompletionFeature.hx │ │ │ ├── CompletionFeatureLegacy.hx │ │ │ ├── ExpectedTypeCompletion.hx │ │ │ ├── PostfixCompletion.hx │ │ │ └── SnippetCompletion.hx │ │ ├── documentSymbols │ │ │ ├── DocumentSymbolsFeature.hx │ │ │ ├── DocumentSymbolsResolver.hx │ │ │ └── SymbolStack.hx │ │ └── foldingRange │ │ │ ├── FoldingRangeFeature.hx │ │ │ └── FoldingRangeResolver.hx │ └── hxml │ │ ├── CompletionFeature.hx │ │ ├── HoverFeature.hx │ │ ├── HxmlContextAnalyzer.hx │ │ └── data │ │ ├── Defines.hx │ │ ├── Flags.hx │ │ └── Shared.hx │ ├── helper │ ├── DisplayOffsetConverter.hx │ ├── DocHelper.hx │ ├── FormatterHelper.hx │ ├── FsHelper.hx │ ├── HaxePosition.hx │ ├── IdentifierHelper.hx │ ├── ImportHelper.hx │ ├── JavadocHelper.hx │ ├── PathHelper.hx │ ├── SemVer.hx │ ├── Set.hx │ ├── SnippetHelper.hx │ ├── StructDefaultsMacro.hx │ ├── TypeHelper.hx │ ├── VscodeCommands.hx │ └── WorkspaceEditHelper.hx │ ├── hxParser │ ├── PositionAwareWalker.hx │ └── RenameResolver.hx │ ├── import.hx │ ├── protocol │ ├── CompilerMetadata.hx │ ├── DisplayPrinter.hx │ ├── DotPath.hx │ └── Extensions.hx │ ├── server │ ├── DisplayRequest.hx │ ├── DisplayResult.hx │ ├── HaxeConnection.hx │ ├── HaxeServer.hx │ ├── MessageBuffer.hx │ ├── ResultHandler.hx │ ├── ServerRecording.hx │ └── ServerRecordingTools.hx │ └── tokentree │ ├── PositionAnalyzer.hx │ ├── TokenContext.hx │ └── TokenTreeManager.hx ├── test ├── TestMain.hx ├── haxeLanguageServer │ ├── features │ │ └── haxe │ │ │ └── codeAction │ │ │ ├── ExtractConstantFeatureTest.hx │ │ │ └── OrganizeImportsFeatureTest.hx │ ├── helper │ │ ├── ArrayHelperTest.hx │ │ ├── IdentifierHelperTest.hx │ │ ├── ImportHelperTest.hx │ │ ├── PathHelperTest.hx │ │ ├── PositionHelperTest.hx │ │ ├── RangeHelperTest.hx │ │ ├── SemVerTest.hx │ │ └── TypeHelperTest.hx │ ├── hxParser │ │ └── RenameResolverTest.hx │ ├── protocol │ │ └── ExtensionsTest.hx │ └── tokentree │ │ └── TokenTreeTest.hx ├── import.hx └── testcases │ ├── EditTestCaseMacro.hx │ ├── TestTextEditHelper.hx │ ├── extractConstant │ ├── ExtractConstant_FILE.edittest │ ├── ExtractConstant_HAXE.edittest │ ├── ExtractConstant_HAXE_singlequote.edittest │ ├── ExtractConstant_multiple.edittest │ ├── ExtractConstant_umlaut_begin.edittest │ └── ExtractConstant_umlaut_end.edittest │ └── organizeImports │ ├── ConditionalImportsWithPackage.edittest │ ├── ConditionalImportsWithPackage_AA.edittest │ ├── ConditionalImportsWithPackage_NPP.edittest │ ├── ConditionalImportsWithPackage_SLP.edittest │ ├── ImportsWithClass.edittest │ ├── ImportsWithClassWithCommentedOutImport.edittest │ ├── ImportsWithClassWithSpaces.edittest │ ├── ImportsWithClassWithSpacesAndUsing.edittest │ ├── ImportsWithClass_conditional.edittest │ ├── ImportsWithClass_conditional_first.edittest │ ├── ImportsWithPackage.edittest │ ├── ImportsWithPackage_AA.edittest │ ├── ImportsWithPackage_NPP.edittest │ ├── ImportsWithPackage_SLP.edittest │ ├── ImportsWithoutPackage.edittest │ ├── ImportsWithoutPackage_AA.edittest │ ├── ImportsWithoutPackage_NPP.edittest │ └── ImportsWithoutPackage_SLP.edittest └── vshaxe-build.json /.github/workflows/formatting.yml: -------------------------------------------------------------------------------- 1 | name: Formatting 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | check: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | submodules: true 14 | - run: | 15 | npm ci 16 | npx lix run formatter -s . --check 17 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - run: npm ci 11 | - run: npx lix run vshaxe-build -t language-server -t language-server-tests 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | dump/ 3 | node_modules/ 4 | Actual.json 5 | .unittest 6 | -------------------------------------------------------------------------------- /.haxerc: -------------------------------------------------------------------------------- 1 | { 2 | "version": "dd0d6a6", 3 | "resolveLibs": "scoped" 4 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach", 6 | "type": "node", 7 | "request": "attach", 8 | "port": 6004, 9 | "sourceMaps": true, 10 | "outFiles": [ 11 | "${workspaceRoot}/bin/*.js" 12 | ] 13 | }, 14 | { 15 | "type": "node", 16 | "request": "launch", 17 | "name": "Tests", 18 | "program": "${workspaceRoot}/bin/test.js", 19 | "sourceMaps": true, 20 | "outFiles": [ 21 | "${workspaceRoot}/bin/*.js" 22 | ] 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[haxe]": { 3 | "editor.formatOnSave": true, 4 | "editor.formatOnPaste": true, 5 | "editor.codeActionsOnSave": { 6 | "source.sortImports": true 7 | } 8 | }, 9 | "haxe.executable": "auto", 10 | "haxe.importsSortOrder": "all-alphabetical", 11 | "haxeTestExplorer.testCommand": [ 12 | "npx", 13 | "lix", 14 | "run", 15 | "vshaxe-build", 16 | "--target", 17 | "language-server-tests", 18 | "--debug", 19 | "--", 20 | "-lib", 21 | "test-adapter" 22 | ] 23 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "vshaxe-build", 6 | "target": "language-server (debug)", 7 | "group": { 8 | "kind": "build", 9 | "isDefault": true 10 | } 11 | }, 12 | { 13 | "type": "vshaxe-build", 14 | "target": "language-server-tests", 15 | "group": { 16 | "kind": "test", 17 | "isDefault": true 18 | } 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 vshaxe contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Haxe Language Server 2 | 3 | [![CI](https://github.com/vshaxe/haxe-language-server/actions/workflows/main.yml/badge.svg)](https://github.com/vshaxe/haxe-language-server/actions/workflows/main.yml) 4 | 5 | This is a language server implementing [Language Server Protocol](https://github.com/Microsoft/language-server-protocol) for the [Haxe](http://haxe.org/) language. 6 | 7 | The goal of this project is to encapsulate haxe's completion API with all its quirks behind a solid and easy-to-use protocol that can be used by any editor/IDE. 8 | 9 | Used by the [Visual Studio Code Haxe Extension](https://github.com/vshaxe/vshaxe). It has also successfully been used in Neovim and Sublime Text[[1]](https://github.com/vshaxe/vshaxe/issues/171)[[2]](https://github.com/vshaxe/vshaxe/issues/328), but no official extensions exist at this time. 10 | 11 | Note that any issues should be reported to [vshaxe](https://github.com/vshaxe/vshaxe) directly (this is also the reason why the issue tracker is disabled). Pull requests are welcome however! 12 | 13 | **IMPORTANT**: This requires Haxe 3.4.0 or newer due to usage of [`-D display-stdin`](https://github.com/HaxeFoundation/haxe/pull/5120), 14 | [`--wait stdio`](https://github.com/HaxeFoundation/haxe/pull/5188) and tons of other fixes and additions related to IDE support. 15 | 16 | ### Building From Source 17 | 18 | The easiest way to work on the language server is probably to build it as part of the vshaxe VSCode extension as instructed [here](https://github.com/vshaxe/vshaxe/wiki/Installation#from-source) (even if you ultimately want to use it outside of VSCode), which allows for easy debugging. 19 | 20 | However, you can also build it as a standalone project like so: 21 | 22 | ``` 23 | git clone https://github.com/vshaxe/haxe-language-server 24 | cd haxe-language-server 25 | npm ci 26 | npx lix run vshaxe-build -t language-server 27 | ``` 28 | 29 | This creates a `bin/server.js` that can be started with `node server.js`. 30 | 31 | ### Usage with (Neo)vim 32 | 33 | There's a large amount of language client plugins for (Neo)vim, but the best choice currently seems to be [coc.nvim](https://github.com/neoclide/coc.nvim). A `coc-settings.json` that is known to work with haxe-language-server looks like this: 34 | 35 | ```haxe 36 | { 37 | "languageserver": { 38 | "haxe": { 39 | "command": "node", 40 | "args": [""], 41 | "filetypes": ["haxe"], 42 | "trace.server": "verbose", 43 | "initializationOptions": { 44 | "displayArguments": ["build.hxml"] 45 | }, 46 | "settings": { 47 | "haxe.executable": "haxe" 48 | } 49 | } 50 | } 51 | } 52 | ``` 53 | 54 | ### Usage with Kate 55 | 56 | Go to configure Kate (`Ctrl+Shift+,`) » `LSP Client` » `User Server Settings` » Add the following snippet to the JSON config within the `servers` object. Don't forget to change the path to the LSP server. 57 | 58 | ```json 59 | "haxe": { 60 | "command": ["node", ""], 61 | "rootIndicationFileNames": ["*.hx", "*.hxml"], 62 | "url": "https://github.com/vshaxe/haxe-language-server", 63 | "initializationOptions": {"displayArguments": ["build.hxml"]}, 64 | "settings": {"haxe": {"buildCompletionCache": true}}, 65 | "highlightingModeRegex": "^Haxe$" 66 | }, 67 | ``` 68 | 69 | Click `Apply`, you can then close the window. Use `File` » `Reload` or `F5` to reload the project. Accept when it asks you whether you want to start the LSP server. 70 | 71 | Where `` can either be a `server.js` you built from source or simply downloaded as part of the Haxe Visual Studio Code extension (`"//.vscode/extensions/nadako.vshaxe-/bin/server.js"`). 72 | -------------------------------------------------------------------------------- /cases/documentSymbols/Input.hx: -------------------------------------------------------------------------------- 1 | class BreakPositions { 2 | // °𐐀 3 | /* °𐐀 */ 4 | var _ = "°𐐀"; 5 | var _ = ~/°𐐀/; 6 | } 7 | 8 | abstract Abstract(Int) { 9 | inline static var CONSTANT = 5; 10 | 11 | public var abstractPropery(get,never):Int; 12 | 13 | @:op(A * B) 14 | public function repeat(rhs:Int):Abstract { 15 | return this * rhs; 16 | } 17 | 18 | @:op(A + B) function add(rhs:Int):Abstract; 19 | 20 | @:arrayAccess 21 | public inline function get(key:Int) { 22 | return 0; 23 | } 24 | 25 | @:resolve 26 | function resolve(name:String) { 27 | return null; 28 | } 29 | 30 | public function new() {} 31 | 32 | function foo() {} 33 | } 34 | 35 | @:deprecated 36 | class Class { 37 | @:deprecated 38 | inline static var CONSTANT = 5; 39 | 40 | var variable:Int; 41 | 42 | var variableWithBlockInit:Int = { 43 | function foo():Int return 0; 44 | var bar = foo(); 45 | bar; 46 | }; 47 | 48 | var property(default,null):Int; 49 | 50 | final finaleVariable:Int; 51 | 52 | final function finalMethod():Void {} 53 | 54 | @:op(A + B) @:deprecated 55 | public function fakeAdd(rhs:Int):Int { 56 | return 0; 57 | } 58 | 59 | function foo(param1:Int, param2:Int) { 60 | function foo2() { 61 | function foo3() { 62 | var foo4:Int; 63 | } 64 | } 65 | 66 | inline function innerFoo() {} 67 | 68 | var f = function() {} 69 | 70 | var a, b, c = { 71 | var f:Int = 100; 72 | f; 73 | }; 74 | 75 | var array = []; 76 | for (element in array) { 77 | var varInFor; 78 | } 79 | 80 | try {} 81 | catch (exception:Any) { 82 | var varInCatch; 83 | } 84 | 85 | for (_ in 0...100) {} 86 | try {} catch (_:Any) {} 87 | 88 | var _:Int; 89 | 90 | macro class MacroClass { 91 | var macroField:Int; 92 | } 93 | 94 | macro class { 95 | var macroField:Int; 96 | } 97 | 98 | // inserted _ name shouldn't appear 99 | var 100 | // and also shouldn't affect positions 101 | var var maybeIncorrectPos:Int; 102 | } 103 | 104 | function new() {} 105 | } 106 | 107 | interface Interface { 108 | var variable:Int; 109 | function foo():Void; 110 | } 111 | 112 | @:enum abstract EnumAbstract(Int) { 113 | function foo() { 114 | macro class MacroClass { 115 | var macroField:Int; 116 | 117 | function macroFunction() { 118 | var macroVar; 119 | } 120 | } 121 | } 122 | 123 | inline static var CONSTANT = 5; 124 | 125 | var Value1 = 0; 126 | var Value2 = 1; 127 | 128 | @:op(A + B) function add(rhs:Int):Abstract; 129 | } 130 | 131 | enum abstract EnumAbstractHaxe4(Int) { 132 | inline static var CONSTANT = 5; 133 | 134 | var Value1 = 0; 135 | var Value2 = 1; 136 | } 137 | 138 | enum Enum { 139 | Simple; 140 | Complex(i:Int, b:Bool); 141 | } 142 | 143 | typedef TypeAlias = Int; 144 | 145 | typedef TypedefShortFields = { 146 | ?a:Int, 147 | b:Bool 148 | } 149 | 150 | typedef TypedefComplexFields = { 151 | @:optional var a:Int; 152 | var b:Bool; 153 | var ?c:Bool; 154 | function foo(bar:Int):Void; 155 | } 156 | 157 | typedef TypedefExtension = { 158 | >Foo, 159 | ?a:Int, 160 | b:Bool 161 | } 162 | 163 | typedef TypedefIntersectionTypes = A & B & { 164 | a:Bool 165 | } & C & { 166 | b:Int 167 | } 168 | 169 | typedef TypedefToArray = Array<{i:Int}>; 170 | 171 | /** 172 | * Type doc comment 173 | */ 174 | // other comments 175 | // in the way 176 | class Type { 177 | /** var doc comment */ 178 | var variable:Int; 179 | 180 | /** 181 | function doc comment 182 | **/ 183 | function func() { 184 | /** 185 | * local function doc comment (not a thing) 186 | */ 187 | function localFunction() {} 188 | 189 | /** 190 | * local var doc comment (not a thing) 191 | */ 192 | var localVar:Int; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /cases/foldingRange/Expected.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "startLine": 9, 4 | "endLine": 11, 5 | "startCharacter": 0, 6 | "endCharacter": 3, 7 | "kind": "comment" 8 | }, 9 | { 10 | "startLine": 12, 11 | "endLine": 80, 12 | "startCharacter": 10, 13 | "endCharacter": 2 14 | }, 15 | { 16 | "startLine": 13, 17 | "endLine": 15, 18 | "startCharacter": 1, 19 | "endCharacter": 4, 20 | "kind": "comment" 21 | }, 22 | { 23 | "startLine": 16, 24 | "endLine": 79, 25 | "startCharacter": 16, 26 | "endCharacter": 3 27 | }, 28 | { 29 | "startLine": 17, 30 | "endLine": 19, 31 | "startCharacter": 19, 32 | "endCharacter": 9 33 | }, 34 | { 35 | "startLine": 22, 36 | "endLine": 24, 37 | "startCharacter": 20, 38 | "endCharacter": 0 39 | }, 40 | { 41 | "startLine": 30, 42 | "endLine": 31, 43 | "startCharacter": 6, 44 | "endCharacter": 0 45 | }, 46 | { 47 | "startLine": 34, 48 | "endLine": 35, 49 | "startCharacter": 28, 50 | "endCharacter": 18 51 | }, 52 | { 53 | "startLine": 39, 54 | "endLine": 40, 55 | "startCharacter": 7, 56 | "endCharacter": 10 57 | }, 58 | { 59 | "startLine": 41, 60 | "endLine": 42, 61 | "startCharacter": 33, 62 | "endCharacter": 10 63 | }, 64 | { 65 | "startLine": 43, 66 | "endLine": 44, 67 | "startCharacter": 9, 68 | "endCharacter": 10 69 | }, 70 | { 71 | "startLine": 47, 72 | "endLine": 48, 73 | "startCharacter": 7, 74 | "endCharacter": 10 75 | }, 76 | { 77 | "startLine": 49, 78 | "endLine": 54, 79 | "startCharacter": 11, 80 | "endCharacter": 10 81 | }, 82 | { 83 | "startLine": 55, 84 | "endLine": 56, 85 | "startCharacter": 9, 86 | "endCharacter": 10 87 | }, 88 | { 89 | "startLine": 38, 90 | "endLine": 57, 91 | "startCharacter": 6, 92 | "endCharacter": 7 93 | }, 94 | { 95 | "startLine": 60, 96 | "endLine": 62, 97 | "startCharacter": 24, 98 | "endCharacter": 8 99 | }, 100 | { 101 | "startLine": 65, 102 | "endLine": 68, 103 | "startCharacter": 24, 104 | "endCharacter": 19 105 | }, 106 | { 107 | "startLine": 74, 108 | "endLine": 78, 109 | "startCharacter": 13, 110 | "endCharacter": 21 111 | }, 112 | { 113 | "startLine": 75, 114 | "endLine": 76, 115 | "startCharacter": 3, 116 | "endCharacter": 15 117 | }, 118 | { 119 | "startLine": 77, 120 | "endLine": 78, 121 | "startCharacter": 3, 122 | "endCharacter": 21 123 | }, 124 | { 125 | "startLine": 84, 126 | "endLine": 85, 127 | "startCharacter": 17, 128 | "endCharacter": 15, 129 | "kind": "region" 130 | }, 131 | { 132 | "startLine": 87, 133 | "endLine": 88, 134 | "startCharacter": 16, 135 | "endCharacter": 14, 136 | "kind": "region" 137 | }, 138 | { 139 | "startLine": 83, 140 | "endLine": 89, 141 | "startCharacter": 16, 142 | "endCharacter": 14, 143 | "kind": "region" 144 | }, 145 | { 146 | "startLine": 92, 147 | "endLine": 93, 148 | "startCharacter": 14, 149 | "endCharacter": 13, 150 | "kind": "region" 151 | }, 152 | { 153 | "startLine": 96, 154 | "endLine": 97, 155 | "startCharacter": 16, 156 | "endCharacter": 14, 157 | "kind": "region" 158 | }, 159 | { 160 | "startLine": 0, 161 | "endLine": 7, 162 | "startCharacter": 0, 163 | "endCharacter": 13, 164 | "kind": "imports" 165 | } 166 | ] -------------------------------------------------------------------------------- /cases/foldingRange/Input.hx: -------------------------------------------------------------------------------- 1 | import haxeLanguageServer.tokentree.FoldingRangeResolver; 2 | import languageServerProtocol.protocol.FoldingRange; 3 | import jsonrpc.ResponseError; 4 | import jsonrpc.Types.NoData; 5 | import jsonrpc.CancellationToken; 6 | 7 | using StringTools; 8 | using Lambda; 9 | 10 | /** 11 | Doc comment 12 | **/ 13 | class Foo { 14 | /** 15 | * JavaDoc-style doc comment 16 | */ 17 | function bar() { 18 | var someStruct = { 19 | foo: 0, 20 | bar: 1 21 | } 22 | 23 | var emptyStruct = { 24 | 25 | 26 | } 27 | 28 | #if foo 29 | #end 30 | 31 | #if foo 32 | 33 | #end 34 | 35 | #if (haxe_ver >= "4.0.0") 36 | trace("Haxe 4"); 37 | #end 38 | 39 | #if outer 40 | #if inner1 41 | call(); 42 | #elseif (haxe_ver >= "4.0.0") 43 | call(); 44 | #else 45 | call(); 46 | #end 47 | 48 | #if inner1 49 | call(); 50 | #elseif foo 51 | call(); 52 | call(); 53 | call(); 54 | #error "foo" 55 | call(); 56 | #else 57 | call(); 58 | #end 59 | #end 60 | 61 | var mulitlineString = " 62 | lorem 63 | ipsum 64 | "; 65 | 66 | var data:Array = [ 67 | 0, 1, 2, 3, 4, 5, 68 | 6, 7, 8, 9, 0, 1, 69 | 2, 3, 4, 5, 6, 7 70 | ]; 71 | 72 | ""; 73 | []; 74 | 75 | switch foo { 76 | case bar: 77 | trace(bar); 78 | default: 79 | trace("default"); 80 | } 81 | } 82 | } 83 | 84 | // # region name 85 | // # region name 86 | // # endregion 87 | 88 | //# region name 89 | //# endregion 90 | // # endregion 91 | 92 | 93 | // region name 94 | // end region 95 | 96 | 97 | // { region name 98 | // } endregion -------------------------------------------------------------------------------- /cases/hxformat.json: -------------------------------------------------------------------------------- 1 | { 2 | "disableFormatting": true 3 | } -------------------------------------------------------------------------------- /haxe_libraries/formatter.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "haxelib:/formatter#1.14.6" into formatter/1.14.6/haxelib 2 | # @run: haxelib run-dir formatter "${HAXE_LIBCACHE}/formatter/1.14.6/haxelib" 3 | -cp ${HAXE_LIBCACHE}/formatter/1.14.6/haxelib/src 4 | -D formatter=1.14.6 -------------------------------------------------------------------------------- /haxe_libraries/haxe-hxparser.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "gh://github.com/vshaxe/haxe-hxparser#de1042397c85ea18440d2d5c3a1c5d47ba2204d3" into haxe-hxparser/0.0.1/github/de1042397c85ea18440d2d5c3a1c5d47ba2204d3 2 | -cp ${HAXE_LIBCACHE}/haxe-hxparser/0.0.1/github/de1042397c85ea18440d2d5c3a1c5d47ba2204d3/src 3 | -D haxe-hxparser=0.0.1 4 | -------------------------------------------------------------------------------- /haxe_libraries/haxeparser.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "gh://github.com/HaxeCheckstyle/haxeparser#f0a7f07101c14dc32b0964dd52af8dcaa322e178" into haxeparser/4.3.0-rc.1/github/f0a7f07101c14dc32b0964dd52af8dcaa322e178 2 | -lib hxparse 3 | -cp ${HAXE_LIBCACHE}/haxeparser/4.3.0-rc.1/github/f0a7f07101c14dc32b0964dd52af8dcaa322e178/src 4 | -D haxeparser=4.3.0-rc.1 -------------------------------------------------------------------------------- /haxe_libraries/hxjsonast.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "haxelib:/hxjsonast#1.1.0" into hxjsonast/1.1.0/haxelib 2 | -cp ${HAXE_LIBCACHE}/hxjsonast/1.1.0/haxelib/src 3 | -D hxjsonast=1.1.0 -------------------------------------------------------------------------------- /haxe_libraries/hxnodejs.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "haxelib:/hxnodejs#12.1.0" into hxnodejs/12.1.0/haxelib 2 | -cp ${HAXE_LIBCACHE}/hxnodejs/12.1.0/haxelib/src 3 | -D hxnodejs=12.1.0 4 | --macro allowPackage('sys') 5 | # should behave like other target defines and not be defined in macro context 6 | --macro define('nodejs') 7 | --macro _internal.SuppressDeprecated.run() 8 | -------------------------------------------------------------------------------- /haxe_libraries/hxparse.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "gh://github.com/simn/hxparse#32e376f80c4b0e999e9f3229947d4dac2138382b" into hxparse/4.0.1/github/32e376f80c4b0e999e9f3229947d4dac2138382b 2 | -cp ${HAXE_LIBCACHE}/hxparse/4.0.1/github/32e376f80c4b0e999e9f3229947d4dac2138382b/src 3 | -D hxparse=4.0.1 -------------------------------------------------------------------------------- /haxe_libraries/json2object.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "gh://github.com/elnabo/json2object#64b46769a143207f266010997ea24aa9764505ce" into json2object/3.10.0/github/64b46769a143207f266010997ea24aa9764505ce 2 | -lib hxjsonast 3 | -cp ${HAXE_LIBCACHE}/json2object/3.10.0/github/64b46769a143207f266010997ea24aa9764505ce/src 4 | -D json2object=3.10.0 -------------------------------------------------------------------------------- /haxe_libraries/language-server-protocol.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "gh://github.com/vshaxe/language-server-protocol-haxe#a6baa2ddcd792e99b19398048ef95aa00f0aa1f6" into language-server-protocol/3.17.1/github/a6baa2ddcd792e99b19398048ef95aa00f0aa1f6 2 | -cp ${HAXE_LIBCACHE}/language-server-protocol/3.17.1/github/a6baa2ddcd792e99b19398048ef95aa00f0aa1f6/src 3 | -D language-server-protocol=3.17.1 -------------------------------------------------------------------------------- /haxe_libraries/rename.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "haxelib:/rename#2.2.2" into rename/2.2.2/haxelib 2 | -cp ${HAXE_LIBCACHE}/rename/2.2.2/haxelib/src 3 | -D rename=2.2.2 -------------------------------------------------------------------------------- /haxe_libraries/safety.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "haxelib:/safety#1.1.2" into safety/1.1.2/haxelib 2 | -cp ${HAXE_LIBCACHE}/safety/1.1.2/haxelib/src 3 | -D safety=1.1.2 -------------------------------------------------------------------------------- /haxe_libraries/test-adapter.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "haxelib:/test-adapter#2.0.4" into test-adapter/2.0.4/haxelib 2 | -lib json2object 3 | -cp ${HAXE_LIBCACHE}/test-adapter/2.0.4/haxelib/ 4 | -D test-adapter=2.0.4 5 | --macro _testadapter.Macro.init() -------------------------------------------------------------------------------- /haxe_libraries/tokentree.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "haxelib:/tokentree#1.2.8" into tokentree/1.2.8/haxelib 2 | -cp ${HAXE_LIBCACHE}/tokentree/1.2.8/haxelib/src 3 | -D tokentree=1.2.8 -------------------------------------------------------------------------------- /haxe_libraries/uglifyjs.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "haxelib:/uglifyjs#1.0.0" into uglifyjs/1.0.0/haxelib 2 | -cp ${HAXE_LIBCACHE}/uglifyjs/1.0.0/haxelib/src/ 3 | -D uglifyjs=1.0.0 4 | --macro UglifyJS.run() 5 | -------------------------------------------------------------------------------- /haxe_libraries/utest.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "gh://github.com/haxe-utest/utest#5de48a964ca75c8e6321ac9706346a24958af2a4" into utest/1.13.2/github/5de48a964ca75c8e6321ac9706346a24958af2a4 2 | -cp ${HAXE_LIBCACHE}/utest/1.13.2/github/5de48a964ca75c8e6321ac9706346a24958af2a4/src 3 | -D utest=1.13.2 4 | --macro utest.utils.Macro.checkHaxe() 5 | --macro utest.utils.Macro.importEnvSettings() 6 | -------------------------------------------------------------------------------- /haxe_libraries/vscode-json-rpc.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "gh://github.com/vshaxe/vscode-json-rpc#0160f06bc9df1dd0547f2edf23753540db74ed5b" into vscode-json-rpc/1.0.0/github/0160f06bc9df1dd0547f2edf23753540db74ed5b 2 | -cp ${HAXE_LIBCACHE}/vscode-json-rpc/1.0.0/github/0160f06bc9df1dd0547f2edf23753540db74ed5b/src 3 | -D vscode-json-rpc=1.0.0 -------------------------------------------------------------------------------- /haxe_libraries/vshaxe-build.hxml: -------------------------------------------------------------------------------- 1 | -D vshaxe-build=0.0.1 2 | # @install: lix --silent download "gh://github.com/vshaxe/vshaxe-build#39ab9c6315ae76080e5399391c8fa561daec6d55" into vshaxe-build/0.0.1/github/39ab9c6315ae76080e5399391c8fa561daec6d55 3 | # @run: haxelib run-dir vshaxe-build "${HAXE_LIBCACHE}/vshaxe-build/0.0.1/github/39ab9c6315ae76080e5399391c8fa561daec6d55" 4 | -cp ${HAXE_LIBCACHE}/vshaxe-build/0.0.1/github/39ab9c6315ae76080e5399391c8fa561daec6d55/ 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "haxe-language-server", 3 | "version": "0.0.0", 4 | "devDependencies": { 5 | "lix": "^15.12.0", 6 | "terser": "^5.15.0" 7 | }, 8 | "scripts": { 9 | "postinstall": "npx lix download" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /shared/haxeLanguageServer/DisplayServerConfig.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer; 2 | 3 | import haxe.DynamicAccess; 4 | import haxe.display.Server.ConfigurePrintParams; 5 | 6 | typedef DisplayServerConfig = { 7 | var path:String; 8 | var env:DynamicAccess; 9 | var arguments:Array; 10 | var useSocket:Bool; 11 | var print:ConfigurePrintParams; 12 | } 13 | -------------------------------------------------------------------------------- /shared/haxeLanguageServer/ServerRecordingEntryKind.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer; 2 | 3 | enum abstract ServerRecordingEntryKind(String) to String { 4 | var In = "<"; 5 | var Out = ">"; 6 | var Local = "-"; 7 | var Comment = "#"; 8 | } 9 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/Init.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer; 2 | 3 | import haxe.macro.Compiler; 4 | 5 | function run() { 6 | #if debug 7 | Compiler.define("uglifyjs_disabled"); 8 | #end 9 | Compiler.define("uglifyjs_bin", (if (Sys.systemName() == "Windows") "node_modules\\.bin\\terser.cmd" else "./node_modules/.bin/terser")); 10 | } 11 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/Main.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer; 2 | 3 | import js.Node.process; 4 | import jsonrpc.Protocol; 5 | import jsonrpc.node.MessageReader; 6 | import jsonrpc.node.MessageWriter; 7 | 8 | function main() { 9 | final reader = new MessageReader(process.stdin); 10 | final writer = new MessageWriter(process.stdout); 11 | final languageServerProtocol = new Protocol(writer.write); 12 | languageServerProtocol.logError = message -> languageServerProtocol.sendNotification(LogMessageNotification.type, {type: Warning, message: message}); 13 | setupTrace(languageServerProtocol); 14 | final context = new Context(languageServerProtocol); 15 | reader.listen(languageServerProtocol.handleMessage); 16 | 17 | function log(method:String, data:Dynamic) { 18 | if (context.config.sendMethodResults) { 19 | languageServerProtocol.sendNotification(LanguageServerMethods.DidRunMethod, { 20 | kind: Lsp, 21 | method: method, 22 | debugInfo: null, 23 | response: { 24 | result: data 25 | } 26 | }); 27 | } 28 | } 29 | languageServerProtocol.didRespondToRequest = function(request, response) { 30 | log(request.method, { 31 | request: request, 32 | response: response 33 | }); 34 | } 35 | languageServerProtocol.didSendNotification = function(notification) { 36 | if (notification.method != LogMessageNotification.type && !notification.method.startsWith("haxe/")) { 37 | log(notification.method, notification); 38 | } 39 | } 40 | } 41 | 42 | private function setupTrace(languageServerProtocol:Protocol) { 43 | haxe.Log.trace = function(v, ?i) { 44 | final r = [Std.string(v)]; 45 | if (i != null && i.customParams != null) { 46 | for (v in i.customParams) 47 | r.push(Std.string(v)); 48 | } 49 | languageServerProtocol.sendNotification(LogMessageNotification.type, {type: Log, message: r.join(" ")}); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/documents/HaxeDocument.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.documents; 2 | 3 | import haxeLanguageServer.tokentree.TokenTreeManager; 4 | import hxParser.ParseTree; 5 | 6 | class HaxeDocument extends HxTextDocument { 7 | public var parseTree(get, never):Null; 8 | public var tokens(get, never):Null; 9 | 10 | var _parseTree:Null; 11 | var _tokens:Null; 12 | 13 | override function update(events:Array, version:Int) { 14 | super.update(events, version); 15 | _parseTree = null; 16 | _tokens = null; 17 | } 18 | 19 | public inline function byteRangeToRange(byteRange:Range, offsetConverter:DisplayOffsetConverter):Range { 20 | return { 21 | start: bytePositionToPosition(byteRange.start, offsetConverter), 22 | end: bytePositionToPosition(byteRange.end, offsetConverter), 23 | }; 24 | } 25 | 26 | inline function bytePositionToPosition(bytePosition:Position, offsetConverter:DisplayOffsetConverter):Position { 27 | final line = lineAt(bytePosition.line); 28 | return { 29 | line: bytePosition.line, 30 | character: offsetConverter.byteOffsetToCharacterOffset(line, bytePosition.character) 31 | }; 32 | } 33 | 34 | function createParseTree() { 35 | return try switch hxParser.HxParser.parse(content) { 36 | case Success(tree): 37 | new hxParser.Converter(tree).convertResultToFile(); 38 | case Failure(error): 39 | trace('hxparser failed to parse $uri with: \'$error\''); 40 | null; 41 | } catch (e) { 42 | trace('hxParser.Converter failed on $uri with: \'$e\''); 43 | null; 44 | } 45 | } 46 | 47 | function get_parseTree() { 48 | if (_parseTree == null) { 49 | _parseTree = createParseTree(); 50 | } 51 | return _parseTree; 52 | } 53 | 54 | function get_tokens() { 55 | if (_tokens == null) { 56 | try { 57 | _tokens = TokenTreeManager.create(content); 58 | } catch (e) { 59 | // trace('$uri: $e'); 60 | } 61 | } 62 | return _tokens; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/documents/HxmlDocument.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.documents; 2 | 3 | class HxmlDocument extends HxTextDocument {} 4 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/documents/TextDocuments.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.documents; 2 | 3 | @:allow(haxeLanguageServer.Context) 4 | class TextDocuments { 5 | public static inline final syncKind = TextDocumentSyncKind.Incremental; 6 | 7 | final documents = new Map(); 8 | 9 | public function new() {} 10 | 11 | public inline function iterator():Iterator { 12 | return documents.iterator(); 13 | } 14 | 15 | public inline function getHaxe(uri:DocumentUri):Null { 16 | var doc:Null = Std.downcast(documents[uri], HaxeDocument); 17 | if (doc == null && uri.isHaxeFile()) { 18 | // document not opened via client, load it directly from disk 19 | // see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didOpen 20 | doc = new HaxeDocument(uri, "hx", 1, sys.io.File.getContent(uri.toFsPath().toString())); 21 | } 22 | return doc; 23 | } 24 | 25 | public inline function getHxml(uri:DocumentUri):Null { 26 | var doc:Null = Std.downcast(documents[uri], HxmlDocument); 27 | if (doc == null && uri.isHxmlFile()) { 28 | // document not (yet) loaded via client, load it directly from disk 29 | // see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didOpen 30 | doc = new HxmlDocument(uri, "hxml", 1, sys.io.File.getContent(uri.toFsPath().toString())); 31 | } 32 | return doc; 33 | } 34 | 35 | function onDidOpenTextDocument(event:DidOpenTextDocumentParams) { 36 | final td = event.textDocument; 37 | final uri = td.uri; 38 | if (uri.isHaxeFile()) { 39 | documents[td.uri] = new HaxeDocument(td.uri, td.languageId, td.version, td.text); 40 | } else if (uri.isHxmlFile()) { 41 | documents[td.uri] = new HxmlDocument(td.uri, td.languageId, td.version, td.text); 42 | } else { 43 | throw uri + " has unsupported file type (must be .hx or .hxml)"; 44 | } 45 | } 46 | 47 | function onDidChangeTextDocument(event:DidChangeTextDocumentParams) { 48 | final td = event.textDocument; 49 | final changes = event.contentChanges; 50 | if (changes.length == 0) 51 | return; 52 | final document = documents[td.uri]; 53 | if (document != null) { 54 | document.update(changes, td.version); 55 | } 56 | } 57 | 58 | function onDidCloseTextDocument(event:DidCloseTextDocumentParams) { 59 | documents.remove(event.textDocument.uri); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/extensions/ArrayExtensions.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.extensions; 2 | 3 | using Lambda; 4 | 5 | inline function occurrences(a:Array, element:T):Int { 6 | return a.count(e -> e == element); 7 | } 8 | 9 | function equals(a1:Array, a2:Array):Bool { 10 | if (a1 == null && a2 == null) 11 | return true; 12 | if (a1 == null && a2 != null) 13 | return false; 14 | if (a1 != null && a2 == null) 15 | return false; 16 | if (a1.length != a2.length) 17 | return false; 18 | for (i in 0...a1.length) 19 | if (a1[i] != a2[i]) 20 | return false; 21 | return true; 22 | } 23 | 24 | function filterDuplicates(array:Array, filter:(a:T, b:T) -> Bool):Array { 25 | final unique:Array = []; 26 | for (element in array) { 27 | var present = false; 28 | for (unique in unique) 29 | if (filter(unique, element)) 30 | present = true; 31 | if (!present) 32 | unique.push(element); 33 | } 34 | return unique; 35 | } 36 | 37 | inline function unique(array:Array):Array { 38 | return filterDuplicates(array, (e1, e2) -> e1 == e2); 39 | } 40 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/extensions/DocumentUriExtensions.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.extensions; 2 | 3 | private final driveLetterPathRe = ~/^\/[a-zA-Z]:/; 4 | private final uriRe = ~/^(([^:\/?#]+?):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/; 5 | 6 | /** ported from VSCode sources **/ 7 | function toFsPath(uri:DocumentUri):FsPath { 8 | if (!uriRe.match(uri.toString()) || uriRe.matched(2) != "file") 9 | throw 'Invalid uri: $uri'; 10 | 11 | final path = uriRe.matched(5).urlDecode(); 12 | if (driveLetterPathRe.match(path)) 13 | return new FsPath(path.charAt(1).toLowerCase() + path.substr(2)); 14 | else 15 | return new FsPath(path); 16 | } 17 | 18 | function isFile(uri:DocumentUri):Bool { 19 | return uri.toString().startsWith("file://"); 20 | } 21 | 22 | function isUntitled(uri:DocumentUri):Bool { 23 | return uri.toString().startsWith("untitled:"); 24 | } 25 | 26 | function isHaxeFile(uri:DocumentUri):Bool { 27 | return uri.toString().endsWith(".hx"); 28 | } 29 | 30 | function isHxmlFile(uri:DocumentUri):Bool { 31 | return uri.toString().endsWith(".hxml"); 32 | } 33 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/extensions/FsPathExtensions.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.extensions; 2 | 3 | private final upperCaseDriveRe = ~/^(\/)?([A-Z]:)/; 4 | 5 | /** ported from VSCode sources **/ 6 | function toUri(path:FsPath):DocumentUri { 7 | var path = path.toString(); 8 | path = path.replace("\\", "/"); 9 | if (path.fastCodeAt(0) != "/".code) 10 | path = "/" + path; 11 | 12 | final parts = ["file://"]; 13 | 14 | if (upperCaseDriveRe.match(path)) 15 | path = upperCaseDriveRe.matched(1) + upperCaseDriveRe.matched(2).toLowerCase() + upperCaseDriveRe.matchedRight(); 16 | 17 | var lastIdx = 0; 18 | while (true) { 19 | final idx = path.indexOf("/", lastIdx); 20 | if (idx == -1) { 21 | parts.push(urlEncode2(path.substring(lastIdx))); 22 | break; 23 | } 24 | parts.push(urlEncode2(path.substring(lastIdx, idx))); 25 | parts.push("/"); 26 | lastIdx = idx + 1; 27 | } 28 | return new DocumentUri(parts.join("")); 29 | } 30 | 31 | private function urlEncode2(s:String):String { 32 | return ~/[!'()*]/g.map(s.urlEncode(), function(re) { 33 | return "%" + re.matched(0).fastCodeAt(0).hex(); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/extensions/FunctionFormattingConfigExtensions.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.extensions; 2 | 3 | import haxe.display.JsonModuleTypes; 4 | import haxeLanguageServer.Configuration.FunctionFormattingConfig; 5 | 6 | function shouldPrintReturn(config:FunctionFormattingConfig, signature:JsonFunctionSignature) { 7 | if (config.useArrowSyntax == true) { 8 | return false; 9 | } 10 | final returnStyle = config.returnTypeHint; 11 | return returnStyle == Always || (returnStyle == NonVoid && !signature.ret.isVoid()); 12 | } 13 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/extensions/RangeExtensions.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.extensions; 2 | 3 | /** 4 | * Extends `languageServerProtocol.Types.Range` with the 5 | * same utility methods that `vscode.Range` provides 6 | * (`vscode\src\vs\workbench\api\node\extHostTypes.ts`). 7 | */ 8 | /** 9 | * `true` if `start` and `end` are equal. 10 | */ 11 | function isEmpty(range:Range):Bool { 12 | return range.end.isEqual(range.start); 13 | } 14 | 15 | /** 16 | * `true` if `start.line` and `end.line` are equal. 17 | */ 18 | function isSingleLine(range:Range):Bool { 19 | return range.start.line == range.end.line; 20 | } 21 | 22 | /** 23 | * Check if a range is contained in this range. 24 | * 25 | * @param other A range. 26 | * @return `true` if the range is inside or equal 27 | * to this range. 28 | */ 29 | inline function contains(range:Range, other:Range):Bool { 30 | return range.containsPos(other.start) && range.containsPos(other.end); 31 | } 32 | 33 | /** 34 | * Check if a position is contained in this range. 35 | * 36 | * @param pos A position. 37 | * @return `true` if the position is inside or equal 38 | * to this range. 39 | */ 40 | function containsPos(range:Range, pos:Position):Bool { 41 | if (pos.isBefore(range.start)) { 42 | return false; 43 | } 44 | if (range.end.isBefore(pos)) { 45 | return false; 46 | } 47 | return true; 48 | } 49 | 50 | /** 51 | * Intersect `range` with this range and returns a new range or `undefined` 52 | * if the ranges have no overlap. 53 | * 54 | * @param range A range. 55 | * @return A range of the greater start and smaller end positions. Will 56 | * return undefined when there is no overlap. 57 | */ 58 | function intersection(range:Range, other:Range):Null { 59 | final start = PositionStatics.Max(other.start, range.start); 60 | final end = PositionStatics.Min(other.end, range.end); 61 | if (start.isAfter(end)) { 62 | // this happens when there is no overlap: 63 | // |-----| 64 | // |----| 65 | return null; 66 | } 67 | return {start: start, end: end}; 68 | } 69 | 70 | /** 71 | * Compute the union of `other` with this range. 72 | * 73 | * @param other A range. 74 | * @return A range of smaller start position and the greater end position. 75 | */ 76 | function union(range:Range, other:Range):Range { 77 | if (range.contains(other)) { 78 | return range; 79 | } else if (other.contains(range)) { 80 | return other; 81 | } 82 | final start = PositionStatics.Min(other.start, range.start); 83 | final end = PositionStatics.Max(other.end, range.end); 84 | return {start: start, end: end}; 85 | } 86 | 87 | /** 88 | * Derived a new range from this range. 89 | * 90 | * @param start A position that should be used as start. The default value is the [current start](#Range.start). 91 | * @param end A position that should be used as end. The default value is the [current end](#Range.end). 92 | * @return A range derived from this range with the given start and end position. 93 | * If start and end are not different `this` range will be returned. 94 | */ 95 | function with(range:Range, ?start:Position, ?end:Position):Range { 96 | final start:Position = if (start == null) range.start else start; 97 | final end:Position = if (end == null) range.end else end; 98 | 99 | if (start.isEqual(range.start) && end.isEqual(range.end)) { 100 | return range; 101 | } 102 | return {start: start, end: end}; 103 | } 104 | 105 | function isEqual(range:Range, other:Range):Bool { 106 | return range.start.isEqual(other.start) && range.end.isEqual(other.end); 107 | } 108 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/extensions/ResponseErrorExtensions.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.extensions; 2 | 3 | import jsonrpc.ResponseError; 4 | import jsonrpc.Types.NoData; 5 | 6 | function handler(reject:ResponseError->Void) { 7 | return function(error:String) reject(ResponseError.internalError(error)); 8 | } 9 | 10 | function invalidXml(reject:ResponseError->Void, data:String) { 11 | reject(ResponseError.internalError("Invalid xml data: " + data)); 12 | } 13 | 14 | function noTokens(reject:ResponseError->Void) { 15 | reject(ResponseError.internalError("Unable to build token tree")); 16 | } 17 | 18 | function noFittingDocument(reject:ResponseError->Void, uri:DocumentUri) { 19 | reject(ResponseError.internalError('Unable to find document for URI $uri, or feature is not supported for this file type / scheme')); 20 | } 21 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/extensions/StringExtensions.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.extensions; 2 | 3 | inline function occurrences(s:String, of:String) { 4 | return s.length - s.replace(of, "").length; 5 | } 6 | 7 | function untilLastDot(s:String) { 8 | final dotIndex = s.lastIndexOf("."); 9 | if (dotIndex == -1) 10 | return s; 11 | return s.substring(0, dotIndex); 12 | } 13 | 14 | function untilFirstDot(s:String) { 15 | final dotIndex = s.indexOf("."); 16 | if (dotIndex == -1) 17 | return s; 18 | return s.substring(0, dotIndex); 19 | } 20 | 21 | function afterLastDot(s:String) { 22 | final dotIndex = s.lastIndexOf("."); 23 | if (dotIndex == -1) 24 | return s; 25 | return s.substr(dotIndex + 1); 26 | } 27 | 28 | function last(s:String):String { 29 | return s.charAt(s.length - 1); 30 | } 31 | 32 | function capitalize(s:String):String { 33 | return s.charAt(0).toUpperCase() + s.substr(1); 34 | } 35 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/features/CompletionFeature.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.features; 2 | 3 | import haxe.extern.EitherType; 4 | import haxeLanguageServer.features.haxe.completion.CompletionFeature as HaxeCompletionFeature; 5 | import haxeLanguageServer.features.hxml.CompletionFeature as HxmlCompletionFeature; 6 | import jsonrpc.CancellationToken; 7 | import jsonrpc.ResponseError; 8 | import jsonrpc.Types.NoData; 9 | import languageServerProtocol.Types.CompletionItem; 10 | import languageServerProtocol.Types.CompletionList; 11 | 12 | class CompletionFeature { 13 | final haxe:HaxeCompletionFeature; 14 | final hxml:HxmlCompletionFeature; 15 | 16 | public function new(context) { 17 | haxe = new HaxeCompletionFeature(context); 18 | hxml = new HxmlCompletionFeature(context); 19 | 20 | context.languageServerProtocol.onRequest(CompletionRequest.type, onCompletion); 21 | context.languageServerProtocol.onRequest(CompletionResolveRequest.type, onCompletionResolve); 22 | } 23 | 24 | function onCompletion(params:CompletionParams, token:CancellationToken, resolve:Null, CompletionList>>->Void, 25 | reject:ResponseError->Void) { 26 | final uri = params.textDocument.uri; 27 | if (uri.isHaxeFile()) { 28 | haxe.onCompletion(params, token, resolve, reject); 29 | } else if (uri.isHxmlFile()) { 30 | hxml.onCompletion(params, token, resolve, reject); 31 | } else { 32 | reject.noFittingDocument(uri); 33 | } 34 | } 35 | 36 | function onCompletionResolve(item:CompletionItem, token:CancellationToken, resolve:CompletionItem->Void, reject:ResponseError->Void) { 37 | if (item.data != null) { 38 | haxe.onCompletionResolve(item, token, resolve, reject); 39 | } else { 40 | resolve(item); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/features/HoverFeature.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.features; 2 | 3 | import haxeLanguageServer.features.haxe.HoverFeature as HaxeHoverFeature; 4 | import haxeLanguageServer.features.hxml.HoverFeature as HxmlHoverFeature; 5 | import jsonrpc.CancellationToken; 6 | import jsonrpc.ResponseError; 7 | import jsonrpc.Types.NoData; 8 | import languageServerProtocol.Types.Hover; 9 | 10 | class HoverFeature { 11 | final haxe:HaxeHoverFeature; 12 | final hxml:HxmlHoverFeature; 13 | 14 | public function new(context) { 15 | haxe = new HaxeHoverFeature(context); 16 | hxml = new HxmlHoverFeature(context); 17 | 18 | context.languageServerProtocol.onRequest(HoverRequest.type, onHover); 19 | } 20 | 21 | public function onHover(params:TextDocumentPositionParams, token:CancellationToken, resolve:Null->Void, reject:ResponseError->Void) { 22 | final uri = params.textDocument.uri; 23 | if (uri.isHaxeFile()) { 24 | haxe.onHover(params, token, resolve, reject); 25 | } else if (uri.isHxmlFile()) { 26 | hxml.onHover(params, token, resolve, reject); 27 | } else { 28 | reject.noFittingDocument(uri); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/features/haxe/DeterminePackageFeature.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.features.haxe; 2 | 3 | import haxe.display.Display; 4 | import jsonrpc.CancellationToken; 5 | import jsonrpc.ResponseError; 6 | import jsonrpc.Types.NoData; 7 | 8 | class DeterminePackageFeature { 9 | final context:Context; 10 | 11 | public function new(context) { 12 | this.context = context; 13 | context.languageServerProtocol.onRequest(LanguageServerMethods.DeterminePackage, onDeterminePackage); 14 | } 15 | 16 | public function onDeterminePackage(params:{fsPath:String}, token:Null, resolve:{pack:String}->Void, 17 | reject:ResponseError->Void) { 18 | final handle = if (context.haxeServer.supports(DisplayMethods.DeterminePackage)) handleJsonRpc else handleLegacy; 19 | handle(new FsPath(params.fsPath), token, resolve, reject); 20 | } 21 | 22 | function handleJsonRpc(path:FsPath, token:Null, resolve:{pack:String}->Void, reject:ResponseError->Void) { 23 | context.callHaxeMethod(DisplayMethods.DeterminePackage, {file: path}, token, function(result) { 24 | resolve({pack: result.join(".")}); 25 | return null; 26 | }, reject.handler()); 27 | } 28 | 29 | function handleLegacy(path:FsPath, token:Null, resolve:{pack:String}->Void, reject:ResponseError->Void) { 30 | final args = ['$path@0@package']; 31 | context.callDisplay("@package", args, null, token, function(r) { 32 | switch r { 33 | case DCancelled: 34 | resolve({pack: ""}); 35 | case DResult(data): 36 | resolve({pack: data}); 37 | } 38 | }, reject.handler()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/features/haxe/DocumentFormattingFeature.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.features.haxe; 2 | 3 | import formatter.Formatter; 4 | import formatter.codedata.FormatterInputData.FormatterInputRange; 5 | import jsonrpc.CancellationToken; 6 | import jsonrpc.ResponseError; 7 | import jsonrpc.Types.NoData; 8 | 9 | class DocumentFormattingFeature { 10 | final context:Context; 11 | 12 | public function new(context) { 13 | this.context = context; 14 | context.languageServerProtocol.onRequest(DocumentFormattingRequest.type, onDocumentFormatting); 15 | context.languageServerProtocol.onRequest(DocumentRangeFormattingRequest.type, onDocumentRangeFormatting); 16 | } 17 | 18 | function onDocumentFormatting(params:DocumentFormattingParams, token:CancellationToken, resolve:Array->Void, 19 | reject:ResponseError->Void) { 20 | format(params.textDocument.uri, null, resolve, reject); 21 | } 22 | 23 | function onDocumentRangeFormatting(params:DocumentRangeFormattingParams, token:CancellationToken, resolve:Array->Void, 24 | reject:ResponseError->Void) { 25 | format(params.textDocument.uri, params.range, resolve, reject); 26 | } 27 | 28 | function format(uri:DocumentUri, range:Null, resolve:Array->Void, reject:ResponseError->Void) { 29 | final onResolve = context.startTimer("textDocument/formatting"); 30 | final doc:Null = context.documents.getHaxe(uri); 31 | if (doc == null) { 32 | return reject.noFittingDocument(uri); 33 | } 34 | final tokens = doc.tokens; 35 | if (tokens == null) { 36 | return reject.noTokens(); 37 | } 38 | 39 | var path; 40 | var origin; 41 | if (doc.uri.isFile()) { 42 | path = doc.uri.toFsPath().toString(); 43 | origin = SourceFile(path); 44 | } else { 45 | path = context.workspacePath.toString(); 46 | origin = Snippet; 47 | } 48 | final config = Formatter.loadConfig(path); 49 | var inputRange:Null = null; 50 | if (range != null) { 51 | range.start.character = 0; 52 | final converter = new Haxe3DisplayOffsetConverter(); 53 | function convert(position) { 54 | return converter.characterOffsetToByteOffset(doc.content, doc.offsetAt(position)); 55 | } 56 | inputRange = { 57 | startPos: convert(range.start), 58 | endPos: convert(range.end) 59 | } 60 | } 61 | final result = Formatter.format(Tokens(tokens.list, tokens.tree, tokens.bytes, origin), config, inputRange); 62 | switch result { 63 | case Success(formattedCode): 64 | final range:Range = if (range == null) { 65 | { 66 | start: {line: 0, character: 0}, 67 | end: {line: doc.lineCount - 1, character: doc.lineAt(doc.lineCount - 1).length} 68 | } 69 | } else { 70 | range; 71 | } 72 | final edits = if (doc.getText(range) != formattedCode) [{range: range, newText: formattedCode}] else []; 73 | resolve(edits); 74 | onResolve(null, edits.length + " changes"); 75 | case Failure(errorMessage): 76 | reject(ResponseError.internalError(errorMessage)); 77 | case Disabled: 78 | reject(ResponseError.internalError("Formatting is disabled for this file")); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/features/haxe/FindReferencesFeature.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.features.haxe; 2 | 3 | import haxe.display.Display.DisplayMethods; 4 | import haxeLanguageServer.helper.HaxePosition; 5 | import jsonrpc.CancellationToken; 6 | import jsonrpc.ResponseError; 7 | import jsonrpc.Types.NoData; 8 | import languageServerProtocol.Types.Location; 9 | 10 | class FindReferencesFeature { 11 | final context:Context; 12 | 13 | public function new(context) { 14 | this.context = context; 15 | context.languageServerProtocol.onRequest(ReferencesRequest.type, onFindReferences); 16 | } 17 | 18 | function onFindReferences(params:TextDocumentPositionParams, token:CancellationToken, resolve:Null>->Void, 19 | reject:ResponseError->Void) { 20 | final uri = params.textDocument.uri; 21 | final doc = context.documents.getHaxe(uri); 22 | if (doc == null || !uri.isFile()) { 23 | return reject.noFittingDocument(uri); 24 | } 25 | final handle = if (context.haxeServer.supports(DisplayMethods.FindReferences)) handleJsonRpc else handleLegacy; 26 | final offset = context.displayOffsetConverter.characterOffsetToByteOffset(doc.content, doc.offsetAt(params.position)); 27 | handle(params, token, resolve, reject, doc, offset); 28 | } 29 | 30 | function handleJsonRpc(params:TextDocumentPositionParams, token:CancellationToken, resolve:Null>->Void, 31 | reject:ResponseError->Void, doc:HxTextDocument, offset:Int) { 32 | context.callHaxeMethod(DisplayMethods.FindReferences, { 33 | file: doc.uri.toFsPath(), 34 | contents: doc.content, 35 | offset: offset, 36 | kind: WithBaseAndDescendants 37 | }, token, locations -> { 38 | resolve(locations.filter(location -> location != null).map(location -> { 39 | { 40 | uri: location.file.toUri(), 41 | range: location.range 42 | } 43 | })); 44 | return null; 45 | }, reject.handler()); 46 | } 47 | 48 | function handleLegacy(params:TextDocumentPositionParams, token:CancellationToken, resolve:Null>->Void, reject:ResponseError->Void, 49 | doc:HxTextDocument, offset:Int) { 50 | final args = ['${doc.uri.toFsPath()}@$offset@usage']; 51 | context.callDisplay("@usage", args, doc.content, token, function(r) { 52 | switch r { 53 | case DCancelled: 54 | resolve(null); 55 | case DResult(data): 56 | final xml = try Xml.parse(data).firstElement() catch (_:Any) null; 57 | if (xml == null) 58 | return reject.invalidXml(data); 59 | 60 | final positions = [for (el in xml.elements()) el.firstChild().nodeValue]; 61 | if (positions.length == 0) 62 | return resolve([]); 63 | 64 | final results = []; 65 | final haxePosCache = new Map(); 66 | for (pos in positions) { 67 | final location = HaxePosition.parse(pos, doc, haxePosCache, context.displayOffsetConverter); 68 | if (location == null) { 69 | trace("Got invalid position: " + pos); 70 | continue; 71 | } 72 | results.push(location); 73 | } 74 | 75 | resolve(results); 76 | } 77 | }, reject.handler()); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/features/haxe/GotoDefinitionFeature.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.features.haxe; 2 | 3 | import haxe.display.Display; 4 | import haxeLanguageServer.helper.HaxePosition; 5 | import jsonrpc.CancellationToken; 6 | import jsonrpc.ResponseError; 7 | import jsonrpc.Types.NoData; 8 | import languageServerProtocol.Types.DefinitionLink; 9 | 10 | class GotoDefinitionFeature { 11 | final context:Context; 12 | 13 | public function new(context) { 14 | this.context = context; 15 | context.languageServerProtocol.onRequest(DefinitionRequest.type, onGotoDefinition); 16 | } 17 | 18 | public function onGotoDefinition(params:TextDocumentPositionParams, token:CancellationToken, resolve:Array->Void, 19 | reject:ResponseError->Void) { 20 | final uri = params.textDocument.uri; 21 | final doc = context.documents.getHaxe(uri); 22 | if (doc == null || !uri.isFile()) { 23 | return reject.noFittingDocument(uri); 24 | } 25 | final handle = if (context.haxeServer.supports(DisplayMethods.GotoDefinition)) handleJsonRpc else handleLegacy; 26 | final offset = context.displayOffsetConverter.characterOffsetToByteOffset(doc.content, doc.offsetAt(params.position)); 27 | handle(params, token, resolve, reject, doc, offset); 28 | } 29 | 30 | function handleJsonRpc(params:TextDocumentPositionParams, token:CancellationToken, resolve:Array->Void, 31 | reject:ResponseError->Void, doc:HxTextDocument, offset:Int) { 32 | context.callHaxeMethod(DisplayMethods.GotoDefinition, { 33 | file: doc.uri.toFsPath(), 34 | contents: doc.content, 35 | offset: offset 36 | }, token, function(locations) { 37 | resolve(locations.map(location -> { 38 | final document = getHaxeDocument(location.file.toUri()); 39 | final tokens = document!.tokens; 40 | var previewDeclarationRange = location.range; 41 | if (document != null && tokens != null) { 42 | final targetToken = tokens!.getTokenAtOffset(document.offsetAt(location.range.start)); 43 | final pos = targetToken!.parent!.getPos(); 44 | if (pos != null) 45 | previewDeclarationRange = document.rangeAt(pos.min, pos.max); 46 | } 47 | 48 | final link:DefinitionLink = { 49 | targetUri: location.file.toUri(), 50 | targetRange: previewDeclarationRange, 51 | targetSelectionRange: location.range, 52 | }; 53 | link; 54 | })); 55 | return null; 56 | }, reject.handler()); 57 | } 58 | 59 | function getHaxeDocument(uri:DocumentUri):Null { 60 | var document = context.documents.getHaxe(uri); 61 | if (document == null) { 62 | final path = uri.toFsPath().toString(); 63 | if (!sys.FileSystem.exists(path)) 64 | return null; 65 | final content = sys.io.File.getContent(path); 66 | document = new HaxeDocument(uri, "haxe", 0, content); 67 | } 68 | return document; 69 | } 70 | 71 | function handleLegacy(params:TextDocumentPositionParams, token:CancellationToken, resolve:Array->Void, reject:ResponseError->Void, 72 | doc:HxTextDocument, offset:Int) { 73 | final args = ['${doc.uri.toFsPath()}@$offset@position']; 74 | context.callDisplay("@position", args, doc.content, token, function(r) { 75 | switch r { 76 | case DCancelled: 77 | resolve([]); 78 | case DResult(data): 79 | final xml = try Xml.parse(data).firstElement() catch (_:Any) null; 80 | if (xml == null) 81 | return reject.invalidXml(data); 82 | 83 | final positions = [for (el in xml.elements()) el.firstChild().nodeValue]; 84 | if (positions.length == 0) 85 | resolve([]); 86 | final results:Array = []; 87 | for (pos in positions) { 88 | // no cache because this right now only returns one position 89 | final location = HaxePosition.parse(pos, doc, null, context.displayOffsetConverter); 90 | if (location == null) { 91 | trace("Got invalid position: " + pos); 92 | continue; 93 | } 94 | results.push({ 95 | targetUri: location.uri, 96 | targetRange: location.range, 97 | targetSelectionRange: location.range 98 | }); 99 | } 100 | resolve(results); 101 | } 102 | }, reject.handler()); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/features/haxe/GotoImplementationFeature.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.features.haxe; 2 | 3 | import haxe.display.Display; 4 | import jsonrpc.CancellationToken; 5 | import jsonrpc.ResponseError; 6 | import jsonrpc.Types.NoData; 7 | import languageServerProtocol.Types.Definition; 8 | import languageServerProtocol.protocol.Implementation; 9 | 10 | class GotoImplementationFeature { 11 | final context:Context; 12 | 13 | public function new(context) { 14 | this.context = context; 15 | context.languageServerProtocol.onRequest(ImplementationRequest.type, onGotoImplementation); 16 | } 17 | 18 | public function onGotoImplementation(params:TextDocumentPositionParams, token:CancellationToken, resolve:Definition->Void, 19 | reject:ResponseError->Void) { 20 | final uri = params.textDocument.uri; 21 | final doc = context.documents.getHaxe(uri); 22 | if (doc == null || !uri.isFile()) { 23 | return reject.noFittingDocument(uri); 24 | } 25 | final offset = context.displayOffsetConverter.characterOffsetToByteOffset(doc.content, doc.offsetAt(params.position)); 26 | handleJsonRpc(params, token, resolve, reject, doc, offset); 27 | } 28 | 29 | function handleJsonRpc(params:TextDocumentPositionParams, token:CancellationToken, resolve:Definition->Void, reject:ResponseError->Void, 30 | doc:HxTextDocument, offset:Int) { 31 | context.callHaxeMethod(DisplayMethods.GotoImplementation, {file: doc.uri.toFsPath(), contents: doc.content, offset: offset}, token, locations -> { 32 | resolve(locations.map(location -> { 33 | { 34 | uri: location.file.toUri(), 35 | range: location.range 36 | } 37 | })); 38 | return null; 39 | }, reject.handler()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/features/haxe/GotoTypeDefinitionFeature.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.features.haxe; 2 | 3 | import haxe.display.Display; 4 | import jsonrpc.CancellationToken; 5 | import jsonrpc.ResponseError; 6 | import jsonrpc.Types.NoData; 7 | import languageServerProtocol.Types.Definition; 8 | import languageServerProtocol.protocol.TypeDefinition; 9 | 10 | class GotoTypeDefinitionFeature { 11 | final context:Context; 12 | 13 | public function new(context) { 14 | this.context = context; 15 | context.languageServerProtocol.onRequest(TypeDefinitionRequest.type, onGotoTypeDefinition); 16 | } 17 | 18 | public function onGotoTypeDefinition(params:TextDocumentPositionParams, token:CancellationToken, resolve:Definition->Void, 19 | reject:ResponseError->Void) { 20 | final uri = params.textDocument.uri; 21 | final doc = context.documents.getHaxe(uri); 22 | if (doc == null || !uri.isFile()) { 23 | return reject.noFittingDocument(uri); 24 | } 25 | final offset = context.displayOffsetConverter.characterOffsetToByteOffset(doc.content, doc.offsetAt(params.position)); 26 | context.callHaxeMethod(DisplayMethods.GotoTypeDefinition, {file: uri.toFsPath(), contents: doc.content, offset: offset}, token, locations -> { 27 | resolve(locations.map(location -> { 28 | { 29 | uri: location.file.toUri(), 30 | range: location.range 31 | } 32 | })); 33 | return null; 34 | }, reject.handler()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/features/haxe/WorkspaceSymbolsFeature.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.features.haxe; 2 | 3 | import haxeLanguageServer.helper.HaxePosition; 4 | import jsonrpc.CancellationToken; 5 | import jsonrpc.ResponseError; 6 | import jsonrpc.Types.NoData; 7 | import languageServerProtocol.Types.SymbolInformation; 8 | import languageServerProtocol.Types.SymbolKind; 9 | 10 | private enum abstract ModuleSymbolKind(Int) { 11 | final Class = 1; 12 | final Interface; 13 | final Enum; 14 | final TypeAlias; 15 | final Abstract; 16 | final Field; 17 | final Property; 18 | final Method; 19 | final Constructor; 20 | final Function; 21 | final Variable; 22 | final Struct; 23 | final EnumAbstract; 24 | final Operator; 25 | final EnumMember; 26 | final Constant; 27 | final Module; 28 | } 29 | 30 | private typedef ModuleSymbolEntry = { 31 | final name:String; 32 | final kind:ModuleSymbolKind; 33 | final range:Range; 34 | final ?containerName:String; 35 | final ?isDeprecated:Bool; 36 | } 37 | 38 | private typedef SymbolReply = { 39 | final file:FsPath; 40 | final symbols:Array; 41 | } 42 | 43 | class WorkspaceSymbolsFeature { 44 | final context:Context; 45 | 46 | public function new(context) { 47 | this.context = context; 48 | context.languageServerProtocol.onRequest(WorkspaceSymbolRequest.type, onWorkspaceSymbols); 49 | } 50 | 51 | function processSymbolsReply(data:Array, reject:ResponseError->Void) { 52 | final result = []; 53 | for (file in data) { 54 | final uri = HaxePosition.getProperFileNameCase(file.file).toUri(); 55 | for (symbol in file.symbols) { 56 | if (symbol.range == null) { 57 | context.sendShowMessage(Error, "Unknown location for " + haxe.Json.stringify(symbol)); 58 | continue; 59 | } 60 | result.push(moduleSymbolEntryToSymbolInformation(symbol, uri)); 61 | } 62 | } 63 | return result; 64 | } 65 | 66 | function makeRequest(label:String, args:Array, doc:Null, token:CancellationToken, resolve:Array->Void, 67 | reject:ResponseError->Void) { 68 | final onResolve = context.startTimer("@workspace-symbols"); 69 | context.callDisplay(label, args, doc == null ? null : doc.content, token, function(r) { 70 | switch r { 71 | case DCancelled: 72 | resolve([]); 73 | case DResult(data): 74 | final data:Array = try { 75 | haxe.Json.parse(data); 76 | } catch (e) { 77 | reject(ResponseError.internalError("Error parsing document symbol response: " + Std.string(e))); 78 | return; 79 | } 80 | final result = processSymbolsReply(data, reject); 81 | resolve(result); 82 | onResolve(data, data.length + " symbols"); 83 | } 84 | }, reject.handler()); 85 | } 86 | 87 | function onWorkspaceSymbols(params:WorkspaceSymbolParams, token:CancellationToken, resolve:Array->Void, 88 | reject:ResponseError->Void) { 89 | final args = ["?@0@workspace-symbols@" + params.query]; 90 | makeRequest("@workspace-symbols", args, null, token, resolve, reject); 91 | } 92 | 93 | function moduleSymbolEntryToSymbolInformation(entry:ModuleSymbolEntry, uri:DocumentUri):SymbolInformation { 94 | final result:SymbolInformation = { 95 | name: entry.name, 96 | kind: switch entry.kind { 97 | case Class | Abstract: SymbolKind.Class; 98 | case Interface | TypeAlias: SymbolKind.Interface; 99 | case Enum: SymbolKind.Enum; 100 | case Constructor: SymbolKind.Constructor; 101 | case Field: SymbolKind.Field; 102 | case Method: SymbolKind.Method; 103 | case Function: SymbolKind.Function; 104 | case Property: SymbolKind.Property; 105 | case Variable: SymbolKind.Variable; 106 | case Struct: SymbolKind.Struct; 107 | case EnumAbstract: SymbolKind.Enum; 108 | case Operator: SymbolKind.Operator; 109 | case EnumMember: SymbolKind.EnumMember; 110 | case Constant: SymbolKind.Constant; 111 | case Module: SymbolKind.Module; 112 | }, 113 | location: { 114 | uri: uri, 115 | range: entry.range 116 | } 117 | }; 118 | if (entry.containerName != null) { 119 | result.containerName = entry.containerName; 120 | } 121 | if (entry.isDeprecated != null && entry.isDeprecated) { 122 | result.tags = [Deprecated]; 123 | } 124 | return result; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/features/haxe/codeAction/CodeActionFeature.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.features.haxe.codeAction; 2 | 3 | import haxeLanguageServer.features.haxe.codeAction.diagnostics.MissingArgumentsAction; 4 | import jsonrpc.CancellationToken; 5 | import jsonrpc.ResponseError; 6 | import jsonrpc.Types.NoData; 7 | import languageServerProtocol.Types.CodeAction; 8 | import languageServerProtocol.Types.Diagnostic; 9 | 10 | interface CodeActionContributor { 11 | function createCodeActions(params:CodeActionParams):Array; 12 | } 13 | 14 | enum CodeActionResolveType { 15 | MissingArg; 16 | } 17 | 18 | typedef CodeActionResolveData = { 19 | ?type:CodeActionResolveType, 20 | params:CodeActionParams, 21 | diagnostic:Diagnostic 22 | } 23 | 24 | class CodeActionFeature { 25 | public static inline final SourceSortImports = "source.sortImports"; 26 | 27 | final context:Context; 28 | final contributors:Array = []; 29 | 30 | public function new(context) { 31 | this.context = context; 32 | 33 | context.registerCapability(CodeActionRequest.type, { 34 | documentSelector: Context.haxeSelector, 35 | codeActionKinds: [ 36 | QuickFix, 37 | SourceOrganizeImports, 38 | SourceSortImports, 39 | RefactorExtract, 40 | RefactorRewrite 41 | ], 42 | resolveProvider: true 43 | }); 44 | context.languageServerProtocol.onRequest(CodeActionRequest.type, onCodeAction); 45 | context.languageServerProtocol.onRequest(CodeActionResolveRequest.type, onCodeActionResolve); 46 | 47 | registerContributor(new ExtractConstantFeature(context)); 48 | registerContributor(new DiagnosticsCodeActionFeature(context)); 49 | #if debug 50 | registerContributor(new ExtractTypeFeature(context)); 51 | registerContributor(new ExtractFunctionFeature(context)); 52 | #end 53 | } 54 | 55 | public function registerContributor(contributor:CodeActionContributor) { 56 | contributors.push(contributor); 57 | } 58 | 59 | function onCodeAction(params:CodeActionParams, token:CancellationToken, resolve:Array->Void, reject:ResponseError->Void) { 60 | var codeActions = []; 61 | for (contributor in contributors) { 62 | codeActions = codeActions.concat(contributor.createCodeActions(params)); 63 | } 64 | resolve(codeActions); 65 | } 66 | 67 | function onCodeActionResolve(action:CodeAction, token:CancellationToken, resolve:CodeAction->Void, reject:ResponseError->Void) { 68 | final data:Null = action.data; 69 | final type = data!.type; 70 | final params = data!.params; 71 | final diagnostic = data!.diagnostic; 72 | if (params == null || diagnostic == null) { 73 | resolve(action); 74 | return; 75 | } 76 | switch (type) { 77 | case null: 78 | resolve(action); 79 | case MissingArg: 80 | final promise = MissingArgumentsAction.createMissingArgumentsAction(context, action, params, diagnostic); 81 | if (promise == null) { 82 | reject(ResponseError.internalError("failed to resolve missing arguments action")); 83 | return; 84 | } 85 | promise.then(action -> { 86 | resolve(action); 87 | final command = action.command; 88 | if (command == null) 89 | return; 90 | context.languageServerProtocol.sendNotification(LanguageServerMethods.ExecuteClientCommand, { 91 | command: command.command, 92 | arguments: command.arguments ?? [] 93 | }); 94 | }).catchError((e) -> reject(e)); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/features/haxe/codeAction/DiagnosticsCodeActionFeature.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.features.haxe.codeAction; 2 | 3 | import haxeLanguageServer.features.haxe.DiagnosticsFeature; 4 | import haxeLanguageServer.features.haxe.codeAction.CodeActionFeature; 5 | import haxeLanguageServer.features.haxe.codeAction.diagnostics.CompilerErrorActions; 6 | import haxeLanguageServer.features.haxe.codeAction.diagnostics.MissingFieldsActions; 7 | import haxeLanguageServer.features.haxe.codeAction.diagnostics.OrganizeImportActions; 8 | import haxeLanguageServer.features.haxe.codeAction.diagnostics.ParserErrorActions; 9 | import haxeLanguageServer.features.haxe.codeAction.diagnostics.RemovableCodeActions; 10 | import haxeLanguageServer.features.haxe.codeAction.diagnostics.UnresolvedIdentifierActions; 11 | import haxeLanguageServer.features.haxe.codeAction.diagnostics.UnusedImportActions; 12 | import languageServerProtocol.Types.CodeAction; 13 | 14 | using Lambda; 15 | using tokentree.TokenTreeAccessHelper; 16 | using tokentree.utils.TokenTreeCheckUtils; 17 | 18 | private enum FieldInsertionMode { 19 | IntoClass(rangeClass:Range, rangeEnd:Range); 20 | } 21 | 22 | class DiagnosticsCodeActionFeature implements CodeActionContributor { 23 | final context:Context; 24 | 25 | public function new(context) { 26 | this.context = context; 27 | } 28 | 29 | public function createCodeActions(params:CodeActionParams) { 30 | if (!params.textDocument.uri.isFile()) { 31 | return []; 32 | } 33 | var actions:Array = []; 34 | for (diagnostic in params.context.diagnostics) { 35 | if (diagnostic.code == null || !(diagnostic.code is Int)) { // our codes are int, so we don't handle other stuff 36 | continue; 37 | } 38 | final code = new DiagnosticKind(diagnostic.code); 39 | actions = actions.concat(switch code { 40 | case UnusedImport: UnusedImportActions.createUnusedImportActions(context, params, diagnostic); 41 | case UnresolvedIdentifier: UnresolvedIdentifierActions.createUnresolvedIdentifierActions(context, params, diagnostic); 42 | case CompilerError: CompilerErrorActions.createCompilerErrorActions(context, params, diagnostic); 43 | case RemovableCode: RemovableCodeActions.createRemovableCodeActions(context, params, diagnostic); 44 | case ParserError: ParserErrorActions.createParserErrorActions(context, params, diagnostic); 45 | case MissingFields: MissingFieldsActions.createMissingFieldsActions(context, params, diagnostic); 46 | case _: []; 47 | }); 48 | } 49 | actions = OrganizeImportActions.createOrganizeImportActions(context, params, actions).concat(actions); 50 | actions = actions.filterDuplicates((a, b) -> a.title == b.title); 51 | return actions; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/features/haxe/codeAction/diagnostics/CompilerErrorActions.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.features.haxe.codeAction.diagnostics; 2 | 3 | class CompilerErrorActions { 4 | public static function createCompilerErrorActions(context:Context, params:CodeActionParams, diagnostic:Diagnostic):Array { 5 | if ((params.context.only != null) && (!params.context.only.contains(QuickFix))) { 6 | return []; 7 | } 8 | final actions:Array = []; 9 | final arg = context.diagnostics.getArguments(params.textDocument.uri, CompilerError, diagnostic.range); 10 | if (arg == null) { 11 | return actions; 12 | } 13 | final suggestionsRe = ~/\(Suggestions?: (.*)\)/; 14 | if (suggestionsRe.match(arg)) { 15 | final suggestions = suggestionsRe.matched(1).split(","); 16 | // Haxe reports the entire expression, not just the field position, so we have to be a bit creative here. 17 | final range = diagnostic.range; 18 | final fieldRe = ~/has no field ([^ ]+) /; 19 | if (fieldRe.match(arg)) { 20 | range.start.character = range.end.character - fieldRe.matched(1).length; 21 | } 22 | for (suggestion in suggestions) { 23 | suggestion = suggestion.trim(); 24 | actions.push({ 25 | title: "Change to " + suggestion, 26 | kind: QuickFix, 27 | edit: WorkspaceEditHelper.create(context, params, [{range: range, newText: suggestion}]), 28 | diagnostics: [diagnostic] 29 | }); 30 | } 31 | return actions; 32 | } 33 | 34 | final invalidPackageRe = ~/Invalid package : ([\w.]*) should be ([\w.]*)/; 35 | if (invalidPackageRe.match(arg)) { 36 | final is = invalidPackageRe.matched(1); 37 | final shouldBe = invalidPackageRe.matched(2); 38 | final document = context.documents.getHaxe(params.textDocument.uri); 39 | if (document != null) { 40 | final replacement = document.getText(diagnostic.range).replace(is, shouldBe); 41 | actions.push({ 42 | title: "Change to " + replacement, 43 | kind: QuickFix, 44 | edit: WorkspaceEditHelper.create(context, params, [{range: diagnostic.range, newText: replacement}]), 45 | diagnostics: [diagnostic], 46 | isPreferred: true 47 | }); 48 | } 49 | } 50 | 51 | if (context.haxeServer.haxeVersion.major >= 4 // unsuitable error range before Haxe 4 52 | && arg.contains("should be declared with 'override' since it is inherited from superclass")) { 53 | var pos = diagnostic.range.start; 54 | final document = context.documents.getHaxe(params.textDocument.uri); 55 | if (document.tokens != null) { 56 | // Resolve parent token to add "override" before "fnunction" instead of function name 57 | final funPos = document.tokens!.getTokenAtOffset(document.offsetAt(diagnostic.range.start))!.parent!.pos!.min; 58 | if (funPos != null) { 59 | pos = document.positionAt(funPos); 60 | } 61 | } 62 | actions.push({ 63 | title: "Add override keyword", 64 | kind: QuickFix, 65 | edit: WorkspaceEditHelper.create(context, params, [{range: pos.toRange(), newText: "override "}]), 66 | diagnostics: [diagnostic], 67 | isPreferred: true 68 | }); 69 | } 70 | 71 | final tooManyArgsRe = ~/Too many arguments([\w.]*)/; 72 | if (tooManyArgsRe.match(arg)) { 73 | final document = context.documents.getHaxe(params.textDocument.uri); 74 | final replacement = document.getText(diagnostic.range); 75 | final data:CodeActionResolveData = { 76 | type: MissingArg, 77 | params: params, 78 | diagnostic: diagnostic 79 | }; 80 | actions.push({ 81 | title: "Add argument", 82 | data: data, 83 | kind: RefactorRewrite, 84 | diagnostics: [diagnostic], 85 | isPreferred: false 86 | }); 87 | } 88 | return actions; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/features/haxe/codeAction/diagnostics/OrganizeImportActions.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.features.haxe.codeAction.diagnostics; 2 | 3 | import haxeLanguageServer.features.haxe.DiagnosticsFeature.DiagnosticKind; 4 | import haxeLanguageServer.features.haxe.codeAction.CodeActionFeature; 5 | import haxeLanguageServer.features.haxe.codeAction.OrganizeImportsFeature; 6 | import haxeLanguageServer.helper.DocHelper; 7 | import languageServerProtocol.Types.CodeAction; 8 | 9 | class OrganizeImportActions { 10 | public static function createOrganizeImportActions(context:Context, params:CodeActionParams, existingActions:Array):Array { 11 | var shouldQuickFix:Bool = true; 12 | var shouldOrganize:Bool = true; 13 | var shouldSort:Bool = true; 14 | 15 | if (params.context.only != null) { 16 | shouldQuickFix = params.context.only.contains(QuickFix); 17 | shouldOrganize = params.context.only.contains(SourceOrganizeImports); 18 | shouldSort = params.context.only.contains(CodeActionFeature.SourceSortImports); 19 | } 20 | if (!shouldQuickFix && !shouldOrganize && !shouldSort) { 21 | return []; 22 | } 23 | 24 | final uri = params.textDocument.uri; 25 | final doc = context.documents.getHaxe(uri); 26 | if (doc == null) { 27 | return []; 28 | } 29 | final map = context.diagnostics.getArgumentsMap(uri); 30 | final removeUnusedFixes = if (map == null) [] else [ 31 | for (key in map.keys()) { 32 | if (key.code == DiagnosticKind.UnusedImport) { 33 | WorkspaceEditHelper.removeText(DocHelper.untrimRange(doc, key.range)); 34 | } 35 | } 36 | ]; 37 | 38 | final sortFixes = OrganizeImportsFeature.organizeImports(doc, context, []); 39 | 40 | final unusedRanges:Array = removeUnusedFixes.map(edit -> edit.range); 41 | final organizeFixes = removeUnusedFixes.concat(OrganizeImportsFeature.organizeImports(doc, context, unusedRanges)); 42 | 43 | @:nullSafety(Off) // ? 44 | final diagnostics = existingActions.filter(action -> action.title == DiagnosticsFeature.RemoveUnusedImportUsingTitle) 45 | .map(action -> action.diagnostics) 46 | .flatten() 47 | .array(); 48 | 49 | final actions:Array = []; 50 | 51 | if (shouldOrganize) { 52 | actions.push({ 53 | title: DiagnosticsFeature.OrganizeImportsUsingsTitle, 54 | kind: SourceOrganizeImports, 55 | edit: WorkspaceEditHelper.create(context, params, organizeFixes), 56 | diagnostics: diagnostics 57 | }); 58 | } 59 | if (shouldSort) { 60 | actions.push({ 61 | title: DiagnosticsFeature.SortImportsUsingsTitle, 62 | kind: CodeActionFeature.SourceSortImports, 63 | edit: WorkspaceEditHelper.create(context, params, sortFixes) 64 | }); 65 | } 66 | 67 | if (shouldQuickFix && diagnostics.length > 0 && removeUnusedFixes.length > 1) { 68 | actions.push({ 69 | title: DiagnosticsFeature.RemoveAllUnusedImportsUsingsTitle, 70 | kind: QuickFix, 71 | edit: WorkspaceEditHelper.create(context, params, removeUnusedFixes), 72 | diagnostics: diagnostics 73 | }); 74 | } 75 | 76 | return actions; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/features/haxe/codeAction/diagnostics/RemovableCodeActions.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.features.haxe.codeAction.diagnostics; 2 | 3 | class RemovableCodeActions { 4 | public static function createRemovableCodeActions(context:Context, params:CodeActionParams, diagnostic:Diagnostic):Array { 5 | if ((params.context.only != null) && (!params.context.only.contains(QuickFix))) { 6 | return []; 7 | } 8 | final range = context.diagnostics.getArguments(params.textDocument.uri, RemovableCode, diagnostic.range)!.range; 9 | if (range == null) { 10 | return []; 11 | } 12 | return [ 13 | { 14 | title: "Remove", 15 | kind: QuickFix, 16 | edit: WorkspaceEditHelper.create(context, params, @:nullSafety(Off) [{range: range, newText: ""}]), 17 | diagnostics: [diagnostic], 18 | isPreferred: true 19 | } 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/features/haxe/codeAction/diagnostics/UnresolvedIdentifierActions.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.features.haxe.codeAction.diagnostics; 2 | 3 | import haxeLanguageServer.Configuration; 4 | import haxeLanguageServer.helper.ImportHelper; 5 | import haxeLanguageServer.helper.TypeHelper; 6 | 7 | class UnresolvedIdentifierActions { 8 | public static function createUnresolvedIdentifierActions(context:Context, params:CodeActionParams, diagnostic:Diagnostic):Array { 9 | if ((params.context.only != null) && (!params.context.only.contains(QuickFix))) { 10 | return []; 11 | } 12 | final args = context.diagnostics.getArguments(params.textDocument.uri, UnresolvedIdentifier, diagnostic.range); 13 | if (args == null) { 14 | return []; 15 | } 16 | var actions:Array = []; 17 | final importCount = args.count(a -> a.kind == Import); 18 | for (arg in args) { 19 | actions = actions.concat(switch arg.kind { 20 | case Import: createUnresolvedImportActions(context, params, diagnostic, arg, importCount); 21 | case Typo: createTypoActions(context, params, diagnostic, arg); 22 | }); 23 | } 24 | return actions; 25 | } 26 | 27 | static function createUnresolvedImportActions(context:Context, params:CodeActionParams, diagnostic:Diagnostic, arg, importCount:Int):Array { 28 | final doc = context.documents.getHaxe(params.textDocument.uri); 29 | if (doc == null) { 30 | return []; 31 | } 32 | final preferredStyle = context.config.user.codeGeneration.imports.style; 33 | final secondaryStyle:ImportStyle = if (preferredStyle == Type) Module else Type; 34 | 35 | final importPosition = determineImportPosition(doc); 36 | function makeImportAction(style:ImportStyle):CodeAction { 37 | final path = if (style == Module) TypeHelper.getModule(arg.name) else arg.name; 38 | return { 39 | title: "Import " + path, 40 | kind: QuickFix, 41 | edit: WorkspaceEditHelper.create(context, params, [createImportsEdit(doc, importPosition, [arg.name], style)]), 42 | diagnostics: [diagnostic] 43 | }; 44 | } 45 | 46 | final preferred = makeImportAction(preferredStyle); 47 | final secondary = makeImportAction(secondaryStyle); 48 | if (importCount == 1) { 49 | preferred.isPreferred = true; 50 | } 51 | final actions = [preferred, secondary]; 52 | 53 | actions.push({ 54 | title: "Change to " + arg.name, 55 | kind: QuickFix, 56 | edit: WorkspaceEditHelper.create(context, params, [ 57 | { 58 | range: diagnostic.range, 59 | newText: arg.name 60 | } 61 | ]), 62 | diagnostics: [diagnostic] 63 | }); 64 | 65 | return actions; 66 | } 67 | 68 | static function createTypoActions(context:Context, params:CodeActionParams, diagnostic:Diagnostic, arg):Array { 69 | return [ 70 | { 71 | title: "Change to " + arg.name, 72 | kind: QuickFix, 73 | edit: WorkspaceEditHelper.create(context, params, [{range: diagnostic.range, newText: arg.name}]), 74 | diagnostics: [diagnostic] 75 | } 76 | ]; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/features/haxe/codeAction/diagnostics/UnusedImportActions.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.features.haxe.codeAction.diagnostics; 2 | 3 | import haxeLanguageServer.features.haxe.DiagnosticsFeature.*; 4 | import haxeLanguageServer.helper.DocHelper; 5 | 6 | class UnusedImportActions { 7 | public static function createUnusedImportActions(context:Context, params:CodeActionParams, diagnostic:Diagnostic):Array { 8 | if ((params.context.only != null) && (!params.context.only.contains(QuickFix))) { 9 | return []; 10 | } 11 | final doc = context.documents.getHaxe(params.textDocument.uri); 12 | if (doc == null) { 13 | return []; 14 | } 15 | return [ 16 | { 17 | title: DiagnosticsFeature.RemoveUnusedImportUsingTitle, 18 | kind: QuickFix, 19 | edit: WorkspaceEditHelper.create(context, params, [ 20 | { 21 | range: DocHelper.untrimRange(doc, diagnostic.range), 22 | newText: "" 23 | } 24 | ]), 25 | diagnostics: [diagnostic], 26 | isPreferred: true 27 | } 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/features/haxe/codeAction/diagnostics/import.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.features.haxe.codeAction.diagnostics; 2 | 3 | import haxeLanguageServer.features.haxe.codeAction.CodeActionFeature; 4 | import haxeLanguageServer.helper.WorkspaceEditHelper; 5 | import languageServerProtocol.Types.CodeAction; 6 | import languageServerProtocol.Types.Diagnostic; 7 | 8 | using Lambda; 9 | using tokentree.TokenTreeAccessHelper; 10 | using tokentree.utils.TokenTreeCheckUtils; 11 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/features/haxe/completion/CompletionContextData.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.features.haxe.completion; 2 | 3 | import haxe.display.Display.CompletionMode; 4 | import haxeLanguageServer.helper.ImportHelper.ImportPosition; 5 | import haxeLanguageServer.tokentree.TokenContext; 6 | 7 | typedef CompletionContextData = { 8 | final replaceRange:Range; 9 | final mode:CompletionMode; 10 | final doc:HxTextDocument; 11 | final indent:String; 12 | final lineAfter:String; 13 | final params:CompletionParams; 14 | final importPosition:ImportPosition; 15 | final tokenContext:TokenContext; 16 | var isResolve:Bool; 17 | } 18 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/features/haxe/documentSymbols/DocumentSymbolsFeature.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.features.haxe.documentSymbols; 2 | 3 | import haxe.extern.EitherType; 4 | import jsonrpc.CancellationToken; 5 | import jsonrpc.ResponseError; 6 | import jsonrpc.Types.NoData; 7 | import languageServerProtocol.Types.DocumentSymbol; 8 | import languageServerProtocol.Types.SymbolInformation; 9 | 10 | using Lambda; 11 | 12 | class DocumentSymbolsFeature { 13 | final context:Context; 14 | 15 | public function new(context) { 16 | this.context = context; 17 | context.languageServerProtocol.onRequest(DocumentSymbolRequest.type, onDocumentSymbols); 18 | } 19 | 20 | function onDocumentSymbols(params:DocumentSymbolParams, token:CancellationToken, resolve:Null>>->Void, 21 | reject:ResponseError->Void) { 22 | final onResolve = context.startTimer("textDocument/documentSymbol"); 23 | final uri = params.textDocument.uri; 24 | final doc = context.documents.getHaxe(uri); 25 | if (doc == null) { 26 | return reject.noFittingDocument(uri); 27 | } 28 | if (doc.tokens == null) { 29 | return reject.noTokens(); 30 | } 31 | final symbols = new DocumentSymbolsResolver(doc).resolve(); 32 | resolve(symbols); 33 | onResolve(null, countSymbols(symbols) + " symbols"); 34 | } 35 | 36 | function countSymbols(symbols:Null>):Int { 37 | return if (symbols == null) { 38 | 0; 39 | } else { 40 | symbols.length + symbols.map(symbol -> countSymbols(symbol.children)).fold((a, b) -> a + b, 0); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/features/haxe/documentSymbols/SymbolStack.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.features.haxe.documentSymbols; 2 | 3 | import haxe.display.Display.DisplayModuleTypeKind; 4 | import languageServerProtocol.Types.DocumentSymbol; 5 | 6 | /** (_not_ a video game level, simn) **/ 7 | enum SymbolLevel { 8 | Root; 9 | Type(kind:DisplayModuleTypeKind); 10 | Field; 11 | Expression; 12 | } 13 | 14 | abstract SymbolStack(Array<{level:SymbolLevel, symbol:DocumentSymbol}>) { 15 | public var depth(get, set):Int; 16 | 17 | inline function get_depth() 18 | return this.length - 1; 19 | 20 | function set_depth(newDepth:Int) { 21 | if (newDepth > depth) { 22 | // only accounts for increases of 1 23 | if (this[newDepth] == null) { 24 | this[newDepth] = this[newDepth - 1]; 25 | } 26 | } else if (newDepth < depth) { 27 | while (depth > newDepth) { 28 | this.pop(); 29 | } 30 | } 31 | return depth; 32 | } 33 | 34 | public var level(get, never):SymbolLevel; 35 | 36 | inline function get_level() 37 | return this[depth].level; 38 | 39 | public var root(get, never):DocumentSymbol; 40 | 41 | inline function get_root() 42 | return this[0].symbol; 43 | 44 | public function new() { 45 | this = [ 46 | { 47 | level: Root, 48 | symbol: { 49 | name: "root", 50 | kind: Module, 51 | range: null, 52 | selectionRange: null, 53 | children: [] 54 | } 55 | } 56 | ]; 57 | } 58 | 59 | public function addSymbol(level:SymbolLevel, symbol:DocumentSymbol, opensScope:Bool) { 60 | final parentSymbol = this[depth].symbol; 61 | if (parentSymbol.children == null) { 62 | parentSymbol.children = []; 63 | } 64 | parentSymbol.children.push(symbol); 65 | 66 | if (opensScope) { 67 | this[depth + 1] = {level: level, symbol: symbol}; 68 | } 69 | } 70 | 71 | public function getParentTypeKind():Null { 72 | var i = depth; 73 | while (i-- > 0) { 74 | switch this[i].level { 75 | case Type(kind): 76 | return kind; 77 | case _: 78 | } 79 | } 80 | return null; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/features/haxe/foldingRange/FoldingRangeFeature.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.features.haxe.foldingRange; 2 | 3 | import jsonrpc.CancellationToken; 4 | import jsonrpc.ResponseError; 5 | import jsonrpc.Types.NoData; 6 | import languageServerProtocol.Types.FoldingRange; 7 | import languageServerProtocol.protocol.FoldingRange; 8 | 9 | class FoldingRangeFeature { 10 | final context:Context; 11 | 12 | public function new(context) { 13 | this.context = context; 14 | context.languageServerProtocol.onRequest(FoldingRangeRequest.type, onFoldingRange); 15 | } 16 | 17 | function onFoldingRange(params:FoldingRangeParams, token:CancellationToken, resolve:Array->Void, reject:ResponseError->Void) { 18 | final onResolve = context.startTimer("textDocument/foldingRange"); 19 | final uri = params.textDocument.uri; 20 | final doc = context.documents.getHaxe(uri); 21 | if (doc == null) { 22 | return reject.noFittingDocument(uri); 23 | } 24 | if (doc.tokens == null) { 25 | return reject.noTokens(); 26 | } 27 | final ranges = new FoldingRangeResolver(doc, context.capabilities.textDocument).resolve(); 28 | resolve(ranges); 29 | onResolve(null, ranges.length + " ranges"); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/features/haxe/foldingRange/FoldingRangeResolver.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.features.haxe.foldingRange; 2 | 3 | import languageServerProtocol.Types.FoldingRange; 4 | import languageServerProtocol.Types.FoldingRangeKind; 5 | import tokentree.TokenTree; 6 | 7 | using tokentree.TokenTreeAccessHelper; 8 | 9 | class FoldingRangeResolver { 10 | static final regionStartPattern = ~/^\s*[#{]?\s*region\b/; 11 | static final regionEndPattern = ~/^\s*[#}]?\s*end ?region\b/; 12 | 13 | final document:HaxeDocument; 14 | final lineFoldingOnly:Bool; 15 | 16 | public function new(document:HaxeDocument, capabilities:Null) { 17 | this.document = document; 18 | lineFoldingOnly = capabilities!.foldingRange!.lineFoldingOnly == true; 19 | } 20 | 21 | public function resolve():Array { 22 | final ranges:Array = []; 23 | function add(start:Position, end:Position, ?kind:FoldingRangeKind) { 24 | final range:FoldingRange = { 25 | startLine: start.line, 26 | endLine: end.line 27 | }; 28 | if (!lineFoldingOnly) { 29 | range.startCharacter = start.character; 30 | range.endCharacter = end.character; 31 | } 32 | if (kind != null) { 33 | range.kind = kind; 34 | } 35 | ranges.push(range); 36 | } 37 | 38 | function addRange(range:haxe.macro.Expr.Position, ?kind:FoldingRangeKind) { 39 | final start = document.positionAt(range.min); 40 | final end = document.positionAt(range.max); 41 | add(start, end, kind); 42 | } 43 | 44 | var firstImport:Null = null; 45 | var lastImport:Null = null; 46 | final conditionalStack = []; 47 | final regionStack = []; 48 | final tokens = document.tokens; 49 | if (tokens == null) { 50 | return []; 51 | } 52 | tokens.tree.filterCallback(function(token:TokenTree, _) { 53 | switch token.tok { 54 | case BrOpen, Const(CString(_)), BkOpen: 55 | final range = tokens.getTreePos(token); 56 | final start = document.positionAt(range.min); 57 | final end = getEndOfPreviousLine(range.max); 58 | if (end.line > start.line) { 59 | add(start, end); 60 | } 61 | 62 | case Kwd(KwdCase), Kwd(KwdDefault): 63 | addRange(tokens.getTreePos(token)); 64 | 65 | case Comment(_): 66 | addRange(tokens.getTreePos(token), Comment); 67 | 68 | case CommentLine(s) if (regionStartPattern.match(s)): 69 | regionStack.push(tokens.getPos(token).max); 70 | 71 | case CommentLine(s) if (regionEndPattern.match(s)): 72 | final start = regionStack.pop(); 73 | if (start != null) { 74 | final end = tokens.getPos(token); 75 | @:nullSafety(Off) 76 | addRange({file: end.file, min: start, max: end.max}, Region); 77 | } 78 | 79 | case Kwd(KwdImport), Kwd(KwdUsing): 80 | if (firstImport == null) { 81 | firstImport = token; 82 | } 83 | lastImport = token; 84 | 85 | case Sharp(sharp): 86 | // everything except `#if` ends a range / adds a folding marker 87 | if (sharp == "else" || sharp == "elseif" || sharp == "end") { 88 | final start = conditionalStack.pop(); 89 | final pos = tokens.getPos(token); 90 | final end = getEndOfPreviousLine(pos.max); 91 | if (start != null && end.line > start.line) { 92 | add(start, end); 93 | } 94 | } 95 | 96 | // everything except `#end` starts a range 97 | if (sharp == "if" || sharp == "else" || sharp == "elseif") { 98 | final pClose:Null = token.access().firstChild().matches(POpen).lastChild().matches(PClose).token; 99 | final pos = if (pClose == null) tokens.getPos(token) else tokens.getPos(pClose); 100 | final start = document.positionAt(pos.max); 101 | start.character++; 102 | conditionalStack.push(start); 103 | } 104 | 105 | case _: 106 | } 107 | return GoDeeper; 108 | }); 109 | 110 | if (lastImport != null && firstImport != lastImport) { 111 | final start = tokens.getPos(firstImport); 112 | final end = tokens.getTreePos(lastImport); 113 | addRange({file: start.file, min: start.min, max: end.max}, Imports); 114 | } 115 | 116 | return ranges; 117 | } 118 | 119 | function getEndOfPreviousLine(offset:Int):Position { 120 | final endLine = document.positionAt(offset).line - 1; 121 | final endCharacter = document.lineAt(endLine).length - 1; 122 | return {line: endLine, character: endCharacter}; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/features/hxml/HoverFeature.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.features.hxml; 2 | 3 | import haxeLanguageServer.features.hxml.HxmlContextAnalyzer.analyzeHxmlContext; 4 | import haxeLanguageServer.helper.DocHelper.printCodeBlock; 5 | import jsonrpc.CancellationToken; 6 | import jsonrpc.ResponseError; 7 | import jsonrpc.Types.NoData; 8 | import languageServerProtocol.Types.Hover; 9 | import languageServerProtocol.Types.MarkupKind; 10 | 11 | class HoverFeature { 12 | final context:Context; 13 | 14 | public function new(context) { 15 | this.context = context; 16 | context.languageServerProtocol.onRequest(HoverRequest.type, onHover); 17 | } 18 | 19 | public function onHover(params:TextDocumentPositionParams, token:CancellationToken, resolve:Null->Void, reject:ResponseError->Void) { 20 | final uri = params.textDocument.uri; 21 | final doc = context.documents.getHxml(uri); 22 | if (doc == null) { 23 | return reject.noFittingDocument(uri); 24 | } 25 | final pos = params.position; 26 | final line = doc.lineAt(pos.line); 27 | final hxmlContext = analyzeHxmlContext(line, pos); 28 | function makeHover(sections:Array):Hover { 29 | return { 30 | contents: { 31 | kind: MarkDown, 32 | value: sections.join("\n\n---\n") 33 | }, 34 | range: hxmlContext.range 35 | } 36 | } 37 | resolve(switch hxmlContext.element { 38 | case Flag(flag) if (flag != null): 39 | var signature = flag.name; 40 | if (flag.argument != null) { 41 | signature += " " + flag.argument.name; 42 | } 43 | makeHover([printCodeBlock(signature, Hxml), flag.description]); 44 | 45 | case EnumValue(value, _) if (value != null): 46 | final sections = [printCodeBlock(value.name, Hxml)]; 47 | if (value.description != null) { 48 | sections.push(value.description); 49 | } 50 | makeHover(sections); 51 | 52 | case Define(define) if (define != null): 53 | makeHover([ 54 | printCodeBlock(define.getRealName(), Hxml), 55 | define.printDetails(context.haxeServer.haxeVersion) 56 | ]); 57 | 58 | case DefineValue(define, value): null; 59 | case _: null; 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/features/hxml/HxmlContextAnalyzer.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.features.hxml; 2 | 3 | import haxeLanguageServer.features.hxml.data.Defines; 4 | import haxeLanguageServer.features.hxml.data.Flags; 5 | import haxeLanguageServer.features.hxml.data.Shared; 6 | 7 | using Lambda; 8 | 9 | typedef HxmlContext = { 10 | final element:HxmlElement; 11 | final range:Range; 12 | } 13 | 14 | enum HxmlElement { 15 | Flag(?flag:Flag); 16 | EnumValue(?value:EnumValue, values:EnumValues); 17 | Define(?define:Define); 18 | DefineValue(?define:Define, value:String); 19 | File(?path:String); 20 | Directory(?path:String); 21 | LibraryName(?name:String); 22 | Unknown; 23 | } 24 | 25 | function analyzeHxmlContext(line:String, pos:Position):HxmlContext { 26 | final range = findWordRange(line, pos.character); 27 | line = line.substring(0, range.end); 28 | final parts = ~/\s+/.replace(line.ltrim(), " ").split(" "); 29 | function findFlag(word) { 30 | return HxmlFlags.flatten().find(f -> f.name == word || f.shortName == word || f.deprecatedNames!.contains(word)); 31 | } 32 | return { 33 | element: switch parts { 34 | case []: Flag(); 35 | case [flag]: Flag(findFlag(flag)); 36 | case [flag, arg]: 37 | final flag = findFlag(flag); 38 | switch flag!.argument!.kind { 39 | case null: Unknown; 40 | case Enum(values): EnumValue(values.find(v -> v.name == arg), values); 41 | case Define: 42 | function findDefine(define) { 43 | return getDefines(true).find(d -> d.matches(define)); 44 | } 45 | switch arg.split("=") { 46 | case []: Define(); 47 | case [define]: Define(findDefine(define)); 48 | case [define, value]: 49 | final define = findDefine(define); 50 | final enumValues = define!.getEnumValues(); 51 | if (enumValues != null) { 52 | EnumValue(enumValues.find(v -> v.name == arg), enumValues); 53 | } else { 54 | DefineValue(define, value); 55 | } 56 | case _: Unknown; 57 | } 58 | case File: File(arg); 59 | case Directory: Directory(arg); 60 | case LibraryName: LibraryName(arg); 61 | } 62 | case _: 63 | Unknown; // no completion after the first argument 64 | }, 65 | range: { 66 | start: {line: pos.line, character: range.start}, 67 | end: {line: pos.line, character: range.end} 68 | } 69 | }; 70 | } 71 | 72 | private function findWordRange(s:String, index:Int) { 73 | function isWordBoundary(c:String):Bool { 74 | return c.isSpace(0) || c == "=" || c == ":"; 75 | } 76 | var start = 0; 77 | var end = 0; 78 | var inWord = false; 79 | for (i in 0...s.length) { 80 | final c = s.charAt(i); 81 | if (isWordBoundary(c)) { 82 | if (inWord) { 83 | inWord = false; 84 | end = i; 85 | if (start <= index && end >= index) { 86 | // "Te|xt" 87 | return {start: start, end: end}; 88 | } 89 | } 90 | } else { 91 | if (!inWord) { 92 | inWord = true; 93 | start = i; 94 | } 95 | } 96 | } 97 | // "Text|" 98 | if (inWord) { 99 | return {start: start, end: s.length}; 100 | } 101 | // "Text |" 102 | return {start: index, end: index}; 103 | } 104 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/features/hxml/data/Shared.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.features.hxml.data; 2 | 3 | typedef EnumValue = { 4 | final name:String; 5 | final ?description:String; 6 | } 7 | 8 | typedef EnumValues = ReadOnlyArray; 9 | 10 | final DceEnumValues:EnumValues = [ 11 | { 12 | name: "full", 13 | description: "Apply dead code elimination to all code." 14 | }, 15 | { 16 | name: "std", 17 | description: "Only apply dead code elimination to the standard library." 18 | }, 19 | { 20 | name: "no", 21 | description: "Disable dead code elimination." 22 | } 23 | ]; 24 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/helper/DisplayOffsetConverter.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.helper; 2 | 3 | import js.node.Buffer; 4 | 5 | /** 6 | This is a helper that provides completion between character and bytes offsets. 7 | This is required in Haxe 3.x because it uses byte offsets for positions and display queries. 8 | Haxe 4, however, uses unicode-aware lexer and uses characters for positions, so no 9 | conversion is required. So we have two implementations, one of which is selected based on 10 | Haxe version. 11 | **/ 12 | abstract class DisplayOffsetConverter { 13 | public static function create(haxeVersion:SemVer):DisplayOffsetConverter { 14 | return if (haxeVersion >= new SemVer(4, 0, 0)) new Haxe4DisplayOffsetConverter() else new Haxe3DisplayOffsetConverter(); 15 | } 16 | 17 | public function byteRangeToCharacterRange(range:Range, doc:HxTextDocument):Range { 18 | return { 19 | start: { 20 | line: range.start.line, 21 | character: byteOffsetToCharacterOffset(doc.lineAt(range.start.line), range.start.character) 22 | }, 23 | end: { 24 | line: range.end.line, 25 | character: byteOffsetToCharacterOffset(doc.lineAt(range.end.line), range.end.character) 26 | } 27 | }; 28 | } 29 | 30 | public abstract function positionCharToZeroBasedColumn(char:Int):Int; 31 | 32 | public abstract function byteOffsetToCharacterOffset(string:String, byteOffset:Int):Int; 33 | 34 | public abstract function characterOffsetToByteOffset(string:String, offset:Int):Int; 35 | } 36 | 37 | class Haxe3DisplayOffsetConverter extends DisplayOffsetConverter { 38 | public function new() {} 39 | 40 | function positionCharToZeroBasedColumn(char:Int):Int { 41 | return char; 42 | } 43 | 44 | function byteOffsetToCharacterOffset(string:String, byteOffset:Int):Int { 45 | final buf = Buffer.from(string, "utf-8"); 46 | return buf.toString("utf-8", 0, byteOffset).length; 47 | } 48 | 49 | function characterOffsetToByteOffset(string:String, offset:Int):Int { 50 | if (offset == 0) 51 | return 0; 52 | else if (offset == string.length) 53 | return Buffer.byteLength(string, "utf-8"); 54 | else 55 | return Buffer.byteLength(string.substr(0, offset), "utf-8"); 56 | } 57 | } 58 | 59 | class Haxe4DisplayOffsetConverter extends DisplayOffsetConverter { 60 | public function new() {} 61 | 62 | function positionCharToZeroBasedColumn(char:Int):Int { 63 | return char - 1; 64 | } 65 | 66 | function byteOffsetToCharacterOffset(string:String, offset:Int):Int { 67 | return inline offsetSurrogates(string, offset, 1); 68 | } 69 | 70 | function characterOffsetToByteOffset(string:String, offset:Int):Int { 71 | return inline offsetSurrogates(string, offset, -1); 72 | } 73 | 74 | function offsetSurrogates(string:String, offset:Int, direction:Int):Int { 75 | var ret = offset; 76 | var i = 0, j = 0; 77 | while (j < string.length && i < offset) { 78 | var ch = string.charCodeAt(j).sure(); 79 | if (ch >= 0xD800 && ch < 0xDC00) { 80 | ret += direction; 81 | j++; 82 | } 83 | i++; 84 | j++; 85 | } 86 | return ret; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/helper/DocHelper.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.helper; 2 | 3 | import haxeLanguageServer.helper.JavadocHelper.DocTag; 4 | 5 | class DocHelper { 6 | static final reStartsWhitespace = ~/^\s*/; 7 | static final reEndsWithWhitespace = ~/\s*$/; 8 | 9 | /** Stolen from dox **/ 10 | public static function trim(doc:String) { 11 | if (doc == null) 12 | return ''; 13 | 14 | // trim leading asterisks 15 | while (doc.charAt(0) == '*') 16 | doc = doc.substr(1); 17 | 18 | // trim trailing asterisks 19 | while (doc.charAt(doc.length - 1) == '*') 20 | doc = doc.substr(0, doc.length - 1); 21 | 22 | // trim additional whitespace 23 | doc = doc.trim(); 24 | 25 | // detect doc comment style/indent 26 | final ereg = ~/^([ \t]+(\* )?)[^\s\*]/m; 27 | final matched = ereg.match(doc); 28 | 29 | if (matched) { 30 | var string = ereg.matched(1); 31 | 32 | // escape asterisk and allow one optional space after it 33 | string = string.split('* ').join('\\* ?'); 34 | 35 | final indent = new EReg("^" + string, "gm"); 36 | doc = indent.replace(doc, ""); 37 | } 38 | 39 | // TODO: check why this is necessary (dox doesn't seem to need it...) 40 | if (doc.charAt(0) == '*') 41 | doc = doc.substr(1).ltrim(); 42 | 43 | return doc; 44 | } 45 | 46 | public static function markdownFormat(doc:String):String { 47 | function tableLine(a, b) 48 | return '| $a | $b |\n'; 49 | function tableHeader(a, b) 50 | return "\n" + tableLine(a, b) + tableLine("------", "------"); 51 | function replaceNewlines(s:String, by:String) 52 | return s.replace("\n", by).replace("\r", by); 53 | function mapDocTags(tags:Array) 54 | return tags.map(function(p) { 55 | final desc = replaceNewlines(p.doc, " "); 56 | return tableLine("`" + p.value + "`", desc); 57 | }).join(""); 58 | 59 | doc = trim(doc); 60 | final docInfos = JavadocHelper.parse(doc); 61 | var result = docInfos.doc; 62 | final hasParams = docInfos.params.length > 0; 63 | final hasReturn = docInfos.returns != null; 64 | 65 | result += "\n"; 66 | 67 | if (docInfos.deprecated != null) 68 | result += "\n**Deprecated:** " + docInfos.deprecated.doc + "\n"; 69 | 70 | if (hasParams || hasReturn) 71 | result += tableHeader("Argument", "Description"); 72 | if (hasParams) 73 | result += mapDocTags(docInfos.params); 74 | if (hasReturn) 75 | result += tableLine("`return`", @:nullSafety(Off) replaceNewlines(docInfos.returns.doc, " ")); 76 | 77 | if (docInfos.throws.length > 0) 78 | result += tableHeader("Exception", "Description") + mapDocTags(docInfos.throws); 79 | 80 | if (docInfos.events.length > 0) 81 | result += tableHeader("Event", "Description") + mapDocTags(docInfos.events); 82 | 83 | if (docInfos.sees.length > 0) 84 | result += "\nSee also:\n" + docInfos.sees.map(function(p) return "* " + p.doc).join("\n") + "\n"; 85 | 86 | if (docInfos.since != null) 87 | result += '\n_Available since ${docInfos.since.doc}_'; 88 | 89 | return result; 90 | } 91 | 92 | public static function extractText(doc:String):Null { 93 | if (doc == null) { 94 | return null; 95 | } 96 | var result = ""; 97 | for (line in doc.trim().split("\n")) { 98 | line = line.trim(); 99 | if (line.startsWith("*")) // JavaDoc-style comments 100 | line = line.substr(1); 101 | result += if (line == "") "\n\n" else line + " "; 102 | } 103 | return result; 104 | } 105 | 106 | public static function printCodeBlock(content:String, languageId:LanguageId):String { 107 | return '```$languageId\n$content\n```'; 108 | } 109 | 110 | /** 111 | expands range to encompass full lines when range has leading or trailing whitespace in first and / or last line 112 | 113 | @param doc referenced document 114 | @param range selected range inside document 115 | **/ 116 | public static function untrimRange(doc:HxTextDocument, range:Range) { 117 | final startLine = doc.lineAt(range.start.line); 118 | if (reStartsWhitespace.match(startLine.substring(0, range.start.character))) 119 | range = { 120 | start: { 121 | line: range.start.line, 122 | character: 0 123 | }, 124 | end: range.end 125 | }; 126 | 127 | final endLine = if (range.start.line == range.end.line) startLine else doc.lineAt(range.end.line); 128 | if (reEndsWithWhitespace.match(endLine.substring(range.end.character))) 129 | range = { 130 | start: range.start, 131 | end: { 132 | line: range.end.line + 1, 133 | character: 0 134 | } 135 | }; 136 | return range; 137 | } 138 | } 139 | 140 | enum abstract LanguageId(String) to String { 141 | final Haxe = "haxe"; 142 | final HaxeType = "haxe.type"; 143 | final HaxeArgument = "haxe.argument"; 144 | final Hxml = "hxml"; 145 | } 146 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/helper/FormatterHelper.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.helper; 2 | 3 | import formatter.Formatter; 4 | import tokentree.TokenTreeBuilder; 5 | 6 | class FormatterHelper { 7 | public static function formatText(doc:HxTextDocument, context:Context, code:String, entryPoint:TokenTreeEntryPoint):String { 8 | var path; 9 | var origin; 10 | if (doc.uri.isFile()) { 11 | path = doc.uri.toFsPath().toString(); 12 | origin = SourceFile(path); 13 | } else { 14 | path = context.workspacePath.toString(); 15 | origin = Snippet; 16 | } 17 | final config = Formatter.loadConfig(path); 18 | switch Formatter.format(Code(code, origin), config, null, entryPoint) { 19 | case Success(formattedCode): 20 | return formattedCode; 21 | case Failure(_): 22 | case Disabled: 23 | } 24 | return code; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/helper/FsHelper.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.helper; 2 | 3 | import haxe.io.Path; 4 | import js.lib.Promise; 5 | import js.node.Fs.Fs; 6 | import sys.FileSystem; 7 | import sys.io.File; 8 | 9 | class FsHelper { 10 | public static function cp(source:String, destination:String):Promise { 11 | var promises = new Array>(); 12 | var stats = Fs.lstatSync(source); 13 | 14 | if (stats.isDirectory()) { 15 | if (!FileSystem.exists(destination)) 16 | FileSystem.createDirectory(destination); 17 | var files = Fs.readdirSync(source); 18 | 19 | for (f in files) { 20 | var source = Path.join([source, f]); 21 | var destination = Path.join([destination, f]); 22 | var stats = Fs.statSync(source); 23 | promises.push((if (stats.isDirectory()) cp else copyFile)(source, destination)); 24 | } 25 | } else if (stats.isFile()) { 26 | promises.push(copyFile(source, destination)); 27 | } 28 | 29 | return Promise.all(promises).then((_) -> null); 30 | } 31 | 32 | public static function rmdir(path:String):Promise { 33 | try { 34 | if (!Fs.existsSync(path)) 35 | return Promise.resolve(); 36 | 37 | var stats = Fs.lstatSync(path); 38 | if (!stats.isDirectory()) 39 | return rmFile(path); 40 | 41 | return Promise.all([ 42 | for (f in Fs.readdirSync(path)) 43 | rmdir(Path.join([path, f])) 44 | ]).then((_) -> Fs.rmdirSync(path)); 45 | } catch (err) { 46 | return Promise.reject(err); 47 | } 48 | } 49 | 50 | public static function rmFile(path:String):Promise { 51 | Fs.unlinkSync(path); 52 | return Promise.resolve(); 53 | } 54 | 55 | public static function copyFile(source:String, destination:String):Promise { 56 | var dir = Path.directory(destination); 57 | if (!FileSystem.exists(dir)) 58 | FileSystem.createDirectory(dir); 59 | 60 | File.copy(source, destination); 61 | return Promise.resolve(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/helper/HaxePosition.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.helper; 2 | 3 | import languageServerProtocol.Types.Location; 4 | 5 | class HaxePosition { 6 | static final positionRe = ~/^(.+):(\d+): (?:lines (\d+)-(\d+)|character(?:s (\d+)-| )(\d+))$/; 7 | static final properFileNameCaseCache = new Map(); 8 | static final isWindows = (Sys.systemName() == "Windows"); 9 | 10 | public static function parse(pos:String, doc:HxTextDocument, cache:Null>>, 11 | offsetConverter:DisplayOffsetConverter):Null { 12 | if (!positionRe.match(pos)) 13 | return null; 14 | 15 | final file = getProperFileNameCase(new FsPath(positionRe.matched(1))); 16 | var s = positionRe.matched(3); 17 | if (s != null) { // line span 18 | final startLine = Std.parseInt(s).sure(); 19 | final endLine = Std.parseInt(positionRe.matched(4)); 20 | return { 21 | uri: if (file == doc.uri.toFsPath()) doc.uri else file.toUri(), 22 | range: { 23 | start: {line: startLine - 1, character: 0}, 24 | end: {line: endLine, character: 0}, // don't -1 the end line, since we're pointing to the start of the next line 25 | } 26 | }; 27 | } else { // char span 28 | var line = Std.parseInt(positionRe.matched(2)).sure(); 29 | line--; 30 | 31 | var lineContent, uri; 32 | if (file == doc.uri.toFsPath()) { 33 | // it's a stdin file, we have its content in memory 34 | lineContent = doc.lineAt(line); 35 | uri = doc.uri; 36 | } else { 37 | // we have to read lines from a file on disk (cache if available) 38 | var lines:Null>; 39 | if (cache == null) { 40 | lines = sys.io.File.getContent(file.toString()).split("\n"); 41 | } else { 42 | lines = cache[file]; 43 | if (lines == null) 44 | lines = cache[file] = sys.io.File.getContent(file.toString()).split("\n"); 45 | } 46 | lineContent = lines[line]; 47 | uri = file.toUri(); 48 | } 49 | 50 | final endByte = offsetConverter.positionCharToZeroBasedColumn(Std.parseInt(positionRe.matched(6)).sure()); 51 | final endChar = offsetConverter.byteOffsetToCharacterOffset(lineContent, endByte); 52 | 53 | s = positionRe.matched(5); 54 | final startChar = if (s != null) { 55 | final startByte = offsetConverter.positionCharToZeroBasedColumn(Std.parseInt(s).sure()); 56 | offsetConverter.byteOffsetToCharacterOffset(lineContent, startByte); 57 | } else { 58 | endChar; 59 | } 60 | 61 | return { 62 | uri: uri, 63 | range: { 64 | start: {line: line, character: startChar}, 65 | end: {line: line, character: endChar}, 66 | } 67 | }; 68 | } 69 | } 70 | 71 | public static function getProperFileNameCase(normalizedPath:FsPath):FsPath { 72 | if (!isWindows) { 73 | return normalizedPath; 74 | } 75 | if (properFileNameCaseCache != null) { 76 | final cached = properFileNameCaseCache[normalizedPath]; 77 | if (cached != null) { 78 | return cached; 79 | } 80 | } 81 | var result = normalizedPath; 82 | final parts = normalizedPath.toString().split("\\"); 83 | if (parts.length > 1) { 84 | var acc = parts[0]; 85 | for (i in 1...parts.length) { 86 | var part = parts[i]; 87 | for (realFile in sys.FileSystem.readDirectory(acc + "\\")) { 88 | if (realFile.toLowerCase() == part) { 89 | part = realFile; 90 | break; 91 | } 92 | } 93 | acc = acc + "/" + part; 94 | } 95 | result = new FsPath(acc); 96 | } 97 | return properFileNameCaseCache[normalizedPath] = result; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/helper/IdentifierHelper.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.helper; 2 | 3 | import String.fromCharCode; 4 | import haxeLanguageServer.helper.TypeHelper.DisplayFunctionArgument; 5 | 6 | class IdentifierHelper { 7 | public static function guessNames(args:Array):Array { 8 | return avoidDuplicates([for (arg in args) if (arg.name != null) arg.name else guessName(arg.type)]); 9 | } 10 | 11 | public static function guessName(type:Null):String { 12 | if (type == null) { 13 | return "unknown"; 14 | } 15 | type = TypeHelper.unwrapNullable(type); 16 | type = TypeHelper.getTypeWithoutParams(type); 17 | 18 | return switch type { 19 | case "": "unknown"; 20 | case "Int": "i"; 21 | case "Float": "f"; 22 | case "Bool": "b"; 23 | case "String": "s"; 24 | case "Dynamic": "d"; 25 | case "Null": "n"; 26 | case "True": "t"; 27 | case "False": "f"; 28 | case "Void": "_"; 29 | case type if (type.startsWith("{")): "struct"; 30 | case type: 31 | final segments = ~/(?=[A-Z][^A-Z]*$)/.split(type); 32 | final result = segments[segments.length - 1]; 33 | result.substring(0, 1).toLowerCase() + result.substr(1); 34 | } 35 | } 36 | 37 | public static function avoidDuplicates(names:Array):Array { 38 | final currentOccurrence:Map = new Map(); 39 | return [ 40 | for (name in names) { 41 | var i = currentOccurrence[name]; 42 | if (i == null) 43 | i = 0; 44 | 45 | if (names.occurrences(name) > 1) 46 | i++; 47 | currentOccurrence[name] = i; 48 | 49 | if (i > 0) 50 | name = name + i; 51 | name; 52 | } 53 | ]; 54 | } 55 | 56 | /** 57 | Adds argument names to types from signature completion. 58 | 59 | @param type a type like `(:Int, :Int):Void` or `:Int` 60 | @see https://github.com/HaxeFoundation/haxe/issues/6064 61 | **/ 62 | public static function addNamesToSignatureType(type:String, index:Int = 0):String { 63 | inline function getUniqueLetter(index:Int) { 64 | final letters = 26; 65 | final alphabets = Std.int(index / letters) + 1; 66 | final lowerAsciiA = 0x61; 67 | return [for (i in 0...alphabets) fromCharCode(lowerAsciiA + (index % letters))].join(""); 68 | } 69 | 70 | var isOptional = false; 71 | if (type.startsWith("?")) { 72 | isOptional = true; 73 | type = type.substr(1); 74 | } 75 | 76 | if (type.startsWith(":")) 77 | return (if (isOptional) "?" else "") + getUniqueLetter(index) + type; 78 | else if (type.startsWith("(")) { 79 | final segmentsRe = ~/\((.*?)\)\s*:\s*(.*)/; 80 | if (!segmentsRe.match(type)) 81 | return type; 82 | final args = segmentsRe.matched(1); 83 | final returnType = segmentsRe.matched(2); 84 | final fixedArgs = [ 85 | for (arg in ~/\s*,\s*/g.split(args)) { 86 | final fixedArg = addNamesToSignatureType(arg, index); 87 | index++; 88 | fixedArg; 89 | } 90 | ]; 91 | return '(${fixedArgs.join(", ")}):$returnType'; 92 | } 93 | return type; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/helper/ImportHelper.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.helper; 2 | 3 | import haxe.Json; 4 | import haxe.display.JsonModuleTypes; 5 | import haxeLanguageServer.Configuration.FunctionFormattingConfig; 6 | import haxeLanguageServer.Configuration.ImportStyle; 7 | import haxeLanguageServer.protocol.DisplayPrinter; 8 | import tokentree.TokenTree; 9 | 10 | using Lambda; 11 | using tokentree.TokenTreeAccessHelper; 12 | 13 | typedef ImportPosition = { 14 | final position:Position; 15 | final insertLineBefore:Bool; 16 | final insertLineAfter:Bool; 17 | } 18 | 19 | function createImportsEdit(doc:HxTextDocument, result:ImportPosition, paths:Array, style:ImportStyle):TextEdit { 20 | if (style == Module) { 21 | paths = paths.map(TypeHelper.getModule); 22 | } 23 | final importData = { 24 | range: result.position.toRange(), 25 | newText: paths.map(path -> 'import $path;\n').join("") 26 | }; 27 | function isLineEmpty(delta:Int) { 28 | return doc.lineAt(result.position.line + delta).trim().length == 0; 29 | } 30 | if (result.insertLineBefore && !isLineEmpty(-1)) { 31 | importData.newText = "\n" + importData.newText; 32 | } 33 | if (result.insertLineAfter && !isLineEmpty(0)) { 34 | importData.newText += "\n"; 35 | } 36 | return importData; 37 | } 38 | 39 | function createFunctionImportsEdit(doc:HxTextDocument, result:ImportPosition, context:Context, type:JsonType, 40 | formatting:FunctionFormattingConfig):Array { 41 | final importConfig = context.config.user.codeGeneration.imports; 42 | if (!importConfig.enableAutoImports) { 43 | return []; 44 | } 45 | var paths = []; 46 | final signature = type.extractFunctionSignatureOrThrow(); 47 | if (formatting.argumentTypeHints && (!formatting.useArrowSyntax || signature.args.length != 1)) { 48 | paths = paths.concat(signature.args.map(arg -> arg.t.resolveImports()).flatten().array()); 49 | } 50 | if (formatting.shouldPrintReturn(signature)) { 51 | paths = paths.concat(signature.ret.resolveImports()); 52 | } 53 | paths = paths.filterDuplicates((e1, e2) -> Json.stringify(e1) == Json.stringify(e2)); 54 | 55 | return if (paths.length == 0) { 56 | []; 57 | } else { 58 | final printer = new DisplayPrinter(false, Always); 59 | [createImportsEdit(doc, result, paths.map(printer.printPath), importConfig.style)]; 60 | } 61 | } 62 | 63 | function determineImportPosition(document:HaxeDocument):ImportPosition { 64 | function defaultResult():ImportPosition { 65 | return { 66 | position: {line: 0, character: 0}, 67 | insertLineAfter: true, 68 | insertLineBefore: true 69 | } 70 | } 71 | final tokens = document.tokens; 72 | if (tokens == null) { 73 | return defaultResult(); 74 | } 75 | 76 | var firstImport:Null = null; 77 | var packageStatement:Null = null; 78 | 79 | // if the first token in the file is a comment, we should add the import after this 80 | final firstComment = if (tokens.list[0].tok.match(Comment(_))) { 81 | tokens.list[0]; 82 | } else { 83 | null; 84 | } 85 | 86 | tokens.tree.filterCallback((tree, _) -> { 87 | switch tree.tok { 88 | case Kwd(KwdPackage): 89 | packageStatement = tree; 90 | case Kwd(KwdImport | KwdUsing) | Sharp("if") if (firstImport == null): 91 | firstImport = tree; 92 | case _: 93 | } 94 | return SkipSubtree; 95 | }); 96 | 97 | return if (firstImport != null) { 98 | { 99 | position: document.positionAt(tokens.getPos(firstImport).min), 100 | insertLineBefore: false, 101 | insertLineAfter: false 102 | } 103 | } else if (packageStatement != null) { 104 | final lastChild = packageStatement.getLastChild(); 105 | final tokenPos = tokens.getPos(if (lastChild != null) lastChild else packageStatement); 106 | final pos = document.positionAt(tokenPos.max); 107 | pos.line += 1; 108 | pos.character = 0; 109 | { 110 | position: pos, 111 | insertLineAfter: true, 112 | insertLineBefore: true 113 | } 114 | } else if (firstComment != null) { 115 | final pos = document.positionAt(firstComment.pos.max); 116 | pos.line += 1; 117 | pos.character = 0; 118 | { 119 | position: pos, 120 | insertLineAfter: true, 121 | insertLineBefore: true 122 | } 123 | } else { 124 | defaultResult(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/helper/JavadocHelper.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.helper; 2 | 3 | /** Stolen from dox **/ 4 | class JavadocHelper { 5 | public static function parse(doc:String):DocInfos { 6 | final tags = []; 7 | // TODO: need to parse this better as haxe source might have this sort of meta 8 | final ereg = ~/^@(param|default|exception|throws|deprecated|return|returns|since|see|event|author)\s+([^@]+)/gm; 9 | 10 | doc = ereg.map(doc, function(e) { 11 | final name = e.matched(1); 12 | var doc = e.matched(2); 13 | var value:Null = null; 14 | 15 | switch name { 16 | case 'param', 'exception', 'throws', 'event': 17 | final ereg = ~/([^\s]+)\s+([\s\S]*)/g; 18 | if (ereg.match(doc)) { 19 | value = ereg.matched(1); 20 | doc = ereg.matched(2); 21 | } else { 22 | value = doc; 23 | doc = ""; 24 | } 25 | default: 26 | } 27 | doc = trimDoc(doc); 28 | tags.push({name: name, doc: doc, value: value}); 29 | return ''; 30 | }); 31 | 32 | final infos:DocInfos = { 33 | doc: doc, 34 | throws: [], 35 | params: [], 36 | sees: [], 37 | events: [], 38 | tags: tags 39 | }; 40 | for (tag in tags) 41 | switch tag.name { 42 | case 'param': 43 | infos.params.push(tag); 44 | case 'exception', 'throws': 45 | infos.throws.push(tag); 46 | case 'deprecated': 47 | infos.deprecated = tag; 48 | case 'return', 'returns': 49 | infos.returns = tag; 50 | case 'since': 51 | infos.since = tag; 52 | case 'default': 53 | infos.defaultValue = tag; 54 | case 'see': 55 | infos.sees.push(tag); 56 | case 'event': 57 | infos.events.push(tag); 58 | default: 59 | } 60 | return infos; 61 | } 62 | 63 | static function trimDoc(doc:String) { 64 | final ereg = ~/^\s+/m; 65 | if (ereg.match(doc)) { 66 | final space = new EReg('^' + ereg.matched(0), 'mg'); 67 | doc = space.replace(doc, ''); 68 | } 69 | return doc; 70 | } 71 | } 72 | 73 | typedef DocInfos = { 74 | doc:String, 75 | ?returns:DocTag, 76 | ?deprecated:DocTag, 77 | ?since:DocTag, 78 | ?defaultValue:DocTag, 79 | sees:Array, 80 | params:Array, 81 | throws:Array, 82 | events:Array, 83 | tags:Array 84 | } 85 | 86 | typedef DocTag = { 87 | name:String, 88 | doc:String, 89 | ?value:String 90 | } 91 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/helper/PathHelper.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.helper; 2 | 3 | import haxe.io.Path; 4 | 5 | class PathHelper { 6 | public static function matches(path:FsPath, pathFilter:FsPath):Bool { 7 | return new EReg(pathFilter.toString(), "").match(PathHelper.normalize(path).toString()); 8 | } 9 | 10 | public static function preparePathFilter(diagnosticsPathFilter:String, haxelibPath:Null, workspaceRoot:FsPath):FsPath { 11 | var path = diagnosticsPathFilter; 12 | path = path.replace("${workspaceRoot}", workspaceRoot.toString()); 13 | if (haxelibPath != null) { 14 | path = path.replace("${haxelibPath}", haxelibPath.toString()); 15 | } 16 | return normalize(new FsPath(path)); 17 | } 18 | 19 | static final reUpperCaseDriveLetter = ~/^([A-Z]:)/; 20 | 21 | public static function normalize(path:FsPath):FsPath { 22 | var strPath = Path.normalize(path.toString()); 23 | // we need to make sure the case of the drive letter doesn't matter (C: vs c:) 24 | if (reUpperCaseDriveLetter.match(strPath)) { 25 | final letter = strPath.substr(0, 1).toLowerCase(); 26 | strPath = letter + strPath.substring(1); 27 | } 28 | return new FsPath(strPath); 29 | } 30 | 31 | public static function relativize(path:FsPath, cwd:FsPath):FsPath { 32 | final path = Path.normalize(path.toString()); 33 | final cwd = Path.normalize(cwd.toString()) + "/"; 34 | 35 | final segments = path.split(cwd); 36 | segments.shift(); 37 | return new FsPath(segments.join(cwd)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/helper/SemVer.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.helper; 2 | 3 | private typedef Version = { 4 | final major:Int; 5 | final minor:Int; 6 | final patch:Int; 7 | final ?pre:String; 8 | final ?build:String; 9 | } 10 | 11 | abstract SemVer(Version) from Version { 12 | static final reVersion = ~/^(\d+)\.(\d+)\.(\d+)(?:[-]([a-z0-9.-]+))?(?:[+]([a-z0-9.-]+))?/i; 13 | 14 | public var major(get, never):Int; 15 | 16 | inline function get_major() 17 | return this.major; 18 | 19 | public var minor(get, never):Int; 20 | 21 | inline function get_minor() 22 | return this.minor; 23 | 24 | public var patch(get, never):Int; 25 | 26 | inline function get_patch() 27 | return this.patch; 28 | 29 | /** note: not considered in comparisons or `toString()` **/ 30 | public var pre(get, never):Null; 31 | 32 | inline function get_pre() 33 | return this.pre; 34 | 35 | public var build(get, never):Null; 36 | 37 | inline function get_build() 38 | return this.build; 39 | 40 | public static function parse(s:String):Null { 41 | if (!reVersion.match(s)) { 42 | return null; 43 | } 44 | final major = Std.parseInt(reVersion.matched(1)); 45 | final minor = Std.parseInt(reVersion.matched(2)); 46 | final patch = Std.parseInt(reVersion.matched(3)); 47 | final pre = reVersion.matched(4); 48 | final build = reVersion.matched(5); 49 | return new SemVer(major, minor, patch, pre, build); 50 | } 51 | 52 | inline public function new(major, minor, patch, ?pre, ?build) { 53 | this = { 54 | major: major, 55 | minor: minor, 56 | patch: patch, 57 | pre: pre, 58 | build: build 59 | }; 60 | } 61 | 62 | @:op(a >= b) function isEqualOrGreaterThan(other:SemVer):Bool { 63 | return isEqual(other) || isGreaterThan(other); 64 | } 65 | 66 | @:op(a > b) function isGreaterThan(other:SemVer):Bool { 67 | return (major > other.major) 68 | || (major == other.major && minor > other.minor) 69 | || (major == other.major && minor == other.minor && patch > other.patch); 70 | } 71 | 72 | @:op(a == b) function isEqual(other:SemVer):Bool { 73 | return major == other.major && minor == other.minor && patch == other.patch; 74 | } 75 | 76 | public function toString() { 77 | return '$major.$minor.$patch'; 78 | } 79 | 80 | public function toFullVersion() { 81 | var ret = inline toString(); 82 | if (pre != null) 83 | ret += '-' + pre; 84 | if (build != null) 85 | ret += '+' + build; 86 | return ret; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/helper/Set.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.helper; 2 | 3 | // meh 4 | abstract Set(Map) { 5 | public inline function new() { 6 | this = new Map(); 7 | } 8 | 9 | public inline function add(item:T) { 10 | this[item] = true; 11 | } 12 | 13 | public inline function remove(item:T) { 14 | this[item] = false; 15 | } 16 | 17 | public inline function has(item:T):Bool { 18 | return this[item] == true; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/helper/SnippetHelper.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.helper; 2 | 3 | class SnippetHelper { 4 | public static function prettify(snippet:String):String { 5 | snippet = ~/\$\{\d+:(.*?)\}/g.replace(snippet, "$1"); 6 | return ~/\$\d+/g.replace(snippet, "|"); 7 | } 8 | 9 | public static function offset(snippet:String, offset:Int):String { 10 | return ~/\$\{(\d+)(:.*?)?\}/g.map(snippet, function(regex) { 11 | final id = Std.parseInt(regex.matched(1)); 12 | if (id == null) { 13 | return regex.matched(0); 14 | } 15 | var name = regex.matched(2); 16 | if (name == null) { 17 | name = ""; 18 | } 19 | return '$${${id + offset}$name}'; 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/helper/StructDefaultsMacro.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.helper; 2 | 3 | import haxe.macro.Context; 4 | import haxe.macro.Expr; 5 | import haxe.macro.Type; 6 | 7 | using haxe.macro.TypeTools; 8 | 9 | class StructDefaultsMacro { 10 | /** 11 | Assigns the values from `defaults` to `struct` if they are equal to `null`. 12 | 13 | `struct` and `defaults` are assumed to be structure types. 14 | Assignments are generated recursively for fields that themselves have a structure type. 15 | **/ 16 | public static macro function applyDefaults(struct:Expr, defaults:Expr):Expr { 17 | inline function error(message:String) 18 | Context.fatalError(message, struct.pos); 19 | 20 | final structType = Context.typeof(struct); 21 | final defaultsType = Context.typeof(defaults); 22 | if (!defaultsType.unify(structType)) 23 | error("Arguments don't unify"); 24 | 25 | final fields = getStructFields(structType); 26 | if (fields == null) 27 | error("Unable to retrieve struct fields"); 28 | 29 | return macro { 30 | if ($struct == null) { 31 | $struct = $defaults; 32 | } else { 33 | $b{generateAssignments(fields, struct, defaults)}; 34 | } 35 | } 36 | } 37 | 38 | static function generateAssignments(fields:Array, struct:Expr, defaults:Expr):Array { 39 | var assignments = []; 40 | for (field in fields) { 41 | final name = field.name; 42 | assignments.push(macro { 43 | if ($struct.$name == null) 44 | $struct.$name = $defaults.$name; 45 | }); 46 | 47 | // recurse 48 | switch field.type { 49 | case TType(_, _): 50 | final innerFields = getStructFields(field.type); 51 | if (innerFields != null) 52 | assignments = assignments.concat(generateAssignments(innerFields, macro {$struct.$name;}, macro {$defaults.$name;})); 53 | case _: 54 | } 55 | } 56 | return assignments; 57 | } 58 | 59 | static function getStructFields(type:Type):Null> { 60 | return switch type { 61 | case TType(t, _): 62 | switch t.get().type { 63 | case TAnonymous(a): a.get().fields; 64 | case _: null; 65 | } 66 | case TAbstract(_.get() => a, params) if (a.pack.length == 0 && a.name == "Null"): 67 | getStructFields(params[0]); 68 | case _: null; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/helper/VscodeCommands.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.helper; 2 | 3 | final TriggerSuggest = { 4 | title: "Trigger Suggest", 5 | command: "editor.action.triggerSuggest", 6 | arguments: [] 7 | }; 8 | 9 | final TriggerParameterHints = { 10 | title: "Trigger Parameter Hints", 11 | command: "editor.action.triggerParameterHints", 12 | arguments: [] 13 | }; 14 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/helper/WorkspaceEditHelper.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.helper; 2 | 3 | import haxe.DynamicAccess; 4 | import languageServerProtocol.Types.CreateFile; 5 | import languageServerProtocol.Types.CreateFileKind; 6 | import languageServerProtocol.Types.TextDocumentEdit; 7 | import languageServerProtocol.Types.WorkspaceEdit; 8 | 9 | class WorkspaceEditHelper { 10 | overload public static extern inline function create(context:Context, params:CodeActionParams, edits:Array):WorkspaceEdit { 11 | final doc = context.documents.getHaxe(params.textDocument.uri); 12 | return create(doc, edits); 13 | } 14 | 15 | overload public static extern inline function create(doc:Null, edits:Array):WorkspaceEdit { 16 | final changes = new DynamicAccess>(); 17 | if (doc != null) { 18 | changes[doc.uri.toString()] = edits; 19 | } 20 | return {changes: changes}; 21 | } 22 | 23 | public static function createNewFile(uri:DocumentUri, overwrite:Bool, ignoreIfExists:Bool):CreateFile { 24 | return { 25 | kind: CreateFileKind.Create, 26 | uri: uri, 27 | options: { 28 | overwrite: overwrite, 29 | ignoreIfExists: ignoreIfExists 30 | } 31 | } 32 | } 33 | 34 | public static function textDocumentEdit(uri:DocumentUri, edits:Array):TextDocumentEdit { 35 | return { 36 | textDocument: { 37 | uri: uri, 38 | version: null 39 | }, 40 | edits: edits 41 | } 42 | } 43 | 44 | public static function insertText(pos:Position, newText:String):TextEdit { 45 | return {range: {start: pos, end: pos}, newText: newText}; 46 | } 47 | 48 | public static function replaceText(range:Range, newText:String):TextEdit { 49 | return {range: range, newText: newText}; 50 | } 51 | 52 | public static function removeText(range:Range):TextEdit { 53 | return {range: range, newText: ""}; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/hxParser/PositionAwareWalker.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.hxParser; 2 | 3 | import hxParser.ParseTree; 4 | import hxParser.StackAwareWalker; 5 | import hxParser.WalkStack; 6 | 7 | @:forward(push, pop) 8 | abstract Scope(Array) { 9 | public function new(?tokens:Array) { 10 | this = if (tokens == null) [] else tokens; 11 | } 12 | 13 | public function copy():Scope { 14 | return new Scope(this.copy()); 15 | } 16 | 17 | public function contains(scope:Scope):Bool { 18 | final other:Array = cast scope; 19 | if (this.length > other.length) { 20 | return false; 21 | } 22 | for (i in 0...this.length) { 23 | if (other[i] != this[i]) { 24 | return false; 25 | } 26 | } 27 | return true; 28 | } 29 | 30 | public function equals(scope:Scope):Bool { 31 | final other:Array = cast scope; 32 | return this.equals(other); 33 | } 34 | 35 | public function toString():String { 36 | return this.map(token -> token.text).join(" -> "); 37 | } 38 | } 39 | 40 | class PositionAwareWalker extends StackAwareWalker { 41 | var line:Int = 0; 42 | var character:Int = 0; 43 | final scope = new Scope(); 44 | 45 | override function walkToken(token:Token, stack:WalkStack) { 46 | processTrivia(token.leadingTrivia); 47 | if (token.appearsInSource()) 48 | processToken(token, stack); 49 | processTrivia(token.trailingTrivia); 50 | } 51 | 52 | function processToken(token:Token, stack:WalkStack) { 53 | character += token.text.length; 54 | } 55 | 56 | function processTrivia(trivias:Array) { 57 | for (trivia in trivias) { 58 | final newlines = trivia.text.occurrences("\n"); 59 | if (newlines > 0) { 60 | line += newlines; 61 | character = 0; 62 | } else { 63 | character += trivia.text.length; 64 | } 65 | } 66 | } 67 | 68 | override function walkLiteral_PLiteralString(s:StringToken, stack:WalkStack) { 69 | final string = switch s { 70 | case DoubleQuote(token) | SingleQuote(token): token.text; 71 | } 72 | line += string.occurrences("\n"); 73 | super.walkLiteral_PLiteralString(s, stack); 74 | } 75 | 76 | override function walkEnumDecl(node:EnumDecl, stack:WalkStack) { 77 | scope.push(node.name); 78 | super.walkEnumDecl(node, stack); 79 | closeScope(); 80 | } 81 | 82 | override function walkAbstractDecl(node:AbstractDecl, stack:WalkStack) { 83 | scope.push(node.name); 84 | super.walkAbstractDecl(node, stack); 85 | closeScope(); 86 | } 87 | 88 | override function walkClassDecl(node:ClassDecl, stack:WalkStack) { 89 | scope.push(node.name); 90 | super.walkClassDecl(node, stack); 91 | closeScope(); 92 | } 93 | 94 | override function walkTypedefDecl(node:TypedefDecl, stack:WalkStack) { 95 | scope.push(node.name); 96 | super.walkTypedefDecl(node, stack); 97 | closeScope(); 98 | } 99 | 100 | override function walkFunction(node:Function, stack:WalkStack) { 101 | if (node.name != null) { 102 | scope.push(node.name); 103 | } 104 | super.walkFunction(node, stack); 105 | closeScope(); 106 | } 107 | 108 | override function walkClassField_Function(annotations:NAnnotations, modifiers:Array, functionKeyword:Token, name:Token, 109 | params:Null, parenOpen:Token, args:Null>, parenClose:Token, typeHint:Null, 110 | expr:MethodExpr, stack:WalkStack) { 111 | scope.push(name); 112 | super.walkClassField_Function(annotations, modifiers, functionKeyword, name, params, parenOpen, args, parenClose, typeHint, expr, stack); 113 | closeScope(); 114 | } 115 | 116 | override function walkExpr_EBlock(braceOpen:Token, elems:Array, braceClose:Token, stack:WalkStack) { 117 | scope.push(braceOpen); 118 | super.walkExpr_EBlock(braceOpen, elems, braceClose, stack); 119 | closeScope(); 120 | } 121 | 122 | override function walkExpr_EFor(forKeyword:Token, parenOpen:Token, exprIter:Expr, parenClose:Token, exprBody:Expr, stack:WalkStack) { 123 | scope.push(forKeyword); 124 | super.walkExpr_EFor(forKeyword, parenOpen, exprIter, parenClose, exprBody, stack); 125 | closeScope(); 126 | } 127 | 128 | override function walkCase_Case(caseKeyword:Token, patterns:CommaSeparated, guard:Null, colon:Token, body:Array, 129 | stack:WalkStack) { 130 | scope.push(caseKeyword); 131 | super.walkCase_Case(caseKeyword, patterns, guard, colon, body, stack); 132 | closeScope(); 133 | } 134 | 135 | override function walkCase_Default(defaultKeyword:Token, colon:Token, body:Array, stack:WalkStack) { 136 | scope.push(defaultKeyword); 137 | super.walkCase_Default(defaultKeyword, colon, body, stack); 138 | closeScope(); 139 | } 140 | 141 | function closeScope() { 142 | scope.pop(); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/import.hx: -------------------------------------------------------------------------------- 1 | import haxe.display.FsPath; 2 | import haxe.ds.ReadOnlyArray; 3 | import haxeLanguageServer.documents.*; 4 | import languageServerProtocol.protocol.Protocol; 5 | import languageServerProtocol.textdocument.TextDocument; 6 | 7 | using Safety; 8 | using StringTools; 9 | using haxeLanguageServer.extensions.ArrayExtensions; 10 | using haxeLanguageServer.extensions.DocumentUriExtensions; 11 | using haxeLanguageServer.extensions.FsPathExtensions; 12 | using haxeLanguageServer.extensions.FunctionFormattingConfigExtensions; 13 | using haxeLanguageServer.extensions.PositionExtensions; 14 | using haxeLanguageServer.extensions.RangeExtensions; 15 | using haxeLanguageServer.extensions.ResponseErrorExtensions; 16 | using haxeLanguageServer.extensions.StringExtensions; 17 | using haxeLanguageServer.protocol.DotPath; 18 | using haxeLanguageServer.protocol.Extensions; 19 | 20 | #if !macro 21 | import haxeLanguageServer.helper.DisplayOffsetConverter; 22 | #end 23 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/protocol/CompilerMetadata.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.protocol; 2 | 3 | enum abstract CompilerMetadata(String) to String { 4 | final Op = ":op"; 5 | final Resolve = ":resolve"; 6 | final ArrayAccess = ":arrayAccess"; 7 | final Final = ":final"; 8 | final Optional = ":optional"; 9 | final Enum = ":enum"; 10 | final Value = ":value"; 11 | final Deprecated = ":deprecated"; 12 | final NoCompletion = ":noCompletion"; 13 | final Overload = ":overload"; 14 | } 15 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/protocol/DotPath.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.protocol; 2 | 3 | import haxe.display.JsonModuleTypes.JsonType; 4 | import haxe.display.JsonModuleTypes.JsonTypePath; 5 | import haxeLanguageServer.protocol.DisplayPrinter.PathPrinting; 6 | 7 | enum abstract DotPath(String) { 8 | final Std_Void = "StdTypes.Void"; 9 | final Std_Bool = "StdTypes.Bool"; 10 | final Std_Int = "StdTypes.Int"; 11 | final Std_Float = "StdTypes.Float"; 12 | final Std_Null = "StdTypes.Null"; 13 | final Std_UInt = "UInt"; 14 | final Std_String = "String"; 15 | final Std_Array = "Array"; 16 | final Std_EReg = "EReg"; 17 | final Std_Dynamic = "Dynamic"; 18 | final Haxe_Extern_EitherType = "haxe.extern.EitherType"; 19 | final Haxe_Ds_Map = "haxe.ds.Map"; 20 | final Haxe_Ds_ReadOnlyArray = "haxe.ds.ReadOnlyArray"; 21 | 22 | function new(dotPath) { 23 | this = dotPath; 24 | } 25 | } 26 | 27 | function getDotPath(type:JsonType):Null { 28 | final path = type.getTypePath(); 29 | if (path == null) { 30 | return null; 31 | } 32 | return getDotPath2(path.path); 33 | } 34 | 35 | function getDotPath2(path:JsonTypePath):DotPath { 36 | return @:privateAccess new DotPath(new DisplayPrinter(PathPrinting.Always).printPath(path)); 37 | } 38 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/server/DisplayRequest.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.server; 2 | 3 | import js.node.Buffer; 4 | import jsonrpc.CancellationToken; 5 | 6 | class DisplayRequest { 7 | public final label:String; 8 | public final cancellable:Bool; 9 | public final creationTime:Float; 10 | // these are used for the queue 11 | public var prev:Null; 12 | public var next:Null; 13 | 14 | final args:Array; 15 | final token:Null; 16 | final stdin:Null; 17 | final handler:ResultHandler; 18 | 19 | static final stdinSepBuf = Buffer.alloc(1, 1); 20 | 21 | public function new(label:String, args:Array, ?token:CancellationToken, cancellable:Bool, ?stdin:String, handler:ResultHandler) { 22 | this.label = label; 23 | this.args = args; 24 | this.token = token; 25 | this.cancellable = cancellable; 26 | this.stdin = stdin; 27 | this.handler = handler; 28 | this.creationTime = Date.now().getTime(); 29 | } 30 | 31 | public function prepareBody():Buffer { 32 | if (stdin != null) { 33 | args.push("-D"); 34 | args.push("display-stdin"); 35 | } 36 | 37 | final lenBuf = Buffer.alloc(4); 38 | final chunks = [lenBuf]; 39 | var length = 0; 40 | for (arg in args) { 41 | final buf = Buffer.from(arg + "\n"); 42 | chunks.push(buf); 43 | length += buf.length; 44 | } 45 | 46 | if (stdin != null) { 47 | chunks.push(stdinSepBuf); 48 | final buf = Buffer.from(stdin); 49 | chunks.push(buf); 50 | length += buf.length + stdinSepBuf.length; 51 | } 52 | 53 | lenBuf.writeInt32LE(length, 0); 54 | 55 | return Buffer.concat(chunks, length + 4); 56 | } 57 | 58 | public inline function cancel() { 59 | switch handler { 60 | case Raw(callback) | Processed(callback, _): 61 | callback(DCancelled); 62 | } 63 | } 64 | 65 | public function onData(data:String) { 66 | if (token != null && token.canceled) 67 | return cancel(); 68 | 69 | switch handler { 70 | case Raw(callback): 71 | callback(DResult(data)); 72 | case Processed(callback, errback): 73 | processResult(data, callback, errback); 74 | } 75 | } 76 | 77 | function processResult(data:String, callback:DisplayResult->Void, errback:(error:String) -> Void) { 78 | final buf = new StringBuf(); 79 | var hasError = false; 80 | for (line in data.split("\n")) { 81 | switch line.fastCodeAt(0) { 82 | case 0x01: // print 83 | trace("Haxe print:\n" + line.substring(1).replace("\x01", "\n")); 84 | case 0x02: // error 85 | hasError = true; 86 | default: 87 | buf.add(line); 88 | buf.addChar("\n".code); 89 | } 90 | } 91 | 92 | final data = buf.toString().trim(); 93 | 94 | if (hasError) 95 | return errback(data); 96 | 97 | try { 98 | callback(DResult(data)); 99 | } catch (e) { 100 | errback(jsonrpc.ErrorUtils.errorToString(e, "Exception while handling Haxe completion response: ")); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/server/DisplayResult.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.server; 2 | 3 | enum DisplayResult { 4 | DCancelled; 5 | DResult(msg:String); 6 | } 7 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/server/HaxeConnection.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.server; 2 | 3 | import js.node.Buffer; 4 | import js.node.ChildProcess; 5 | import js.node.Net; 6 | import js.node.child_process.ChildProcess as ChildProcessObject; 7 | import js.node.child_process.ChildProcess.ChildProcessEvent; 8 | import js.node.net.Server.ServerEvent; 9 | import js.node.net.Socket; 10 | import js.node.stream.Readable; 11 | 12 | class HaxeConnection { 13 | static final reTrailingNewline = ~/\r?\n$/; 14 | 15 | final buffer = new MessageBuffer(); 16 | final onMessage:String->Void; 17 | final logger:String->Void; 18 | final process:ChildProcessObject; 19 | var nextMessageLength = -1; 20 | 21 | function new(process, logger, onMessage, onExit) { 22 | this.process = process; 23 | this.logger = logger; 24 | this.onMessage = onMessage; 25 | process.on(ChildProcessEvent.Exit, (_, _) -> onExit(this)); 26 | } 27 | 28 | public function send(data:Buffer) {} 29 | 30 | public function kill() { 31 | process.removeAllListeners(); 32 | process.kill(); 33 | } 34 | 35 | public function getLastErrorOutput():String { 36 | return ""; 37 | } 38 | 39 | function onStdout(buf:Buffer) { 40 | logger(reTrailingNewline.replace(buf.toString(), "")); 41 | } 42 | 43 | function onData(data:Buffer) { 44 | buffer.append(data); 45 | while (true) { 46 | if (nextMessageLength == -1) { 47 | final length = buffer.tryReadLength(); 48 | if (length == -1) 49 | return; 50 | nextMessageLength = length; 51 | } 52 | final msg = buffer.tryReadContent(nextMessageLength); 53 | if (msg == null) 54 | return; 55 | nextMessageLength = -1; 56 | onMessage(msg); 57 | } 58 | } 59 | } 60 | 61 | class StdioConnection extends HaxeConnection { 62 | function new(process, logger, onMessage, onExit) { 63 | super(process, logger, onMessage, onExit); 64 | process.stdout.on(ReadableEvent.Data, onStdout); 65 | process.stderr.on(ReadableEvent.Data, onData); 66 | } 67 | 68 | override function send(data:Buffer) { 69 | process.stdin.write(data); 70 | } 71 | 72 | override function getLastErrorOutput():String { 73 | return buffer.getContent(); 74 | } 75 | 76 | public static function start(path:String, arguments:Array, spawnOptions:ChildProcessSpawnOptions, logger:String->Void, onMessage:String->Void, 77 | onExit:HaxeConnection->Void, callback:HaxeConnection->Void) { 78 | trace("Using --wait stdio"); 79 | final process = ChildProcess.spawn(path, arguments.concat(["--wait", "stdio"]), spawnOptions); 80 | callback(new StdioConnection(process, logger, onMessage, onExit)); 81 | } 82 | } 83 | 84 | class SocketConnection extends HaxeConnection { 85 | var socket:Null; 86 | var lastErrorOutput:String = ""; 87 | 88 | function new(process, logger, onMessage, onExit) { 89 | super(process, logger, onMessage, onExit); 90 | process.stdout.on(ReadableEvent.Data, onStdout); 91 | process.stderr.on(ReadableEvent.Data, onStderr); 92 | } 93 | 94 | function setup(socket:Socket) { 95 | this.socket = socket; 96 | socket.on(ReadableEvent.Data, onData); 97 | } 98 | 99 | override function send(data:Buffer) { 100 | socket!.write(data); 101 | } 102 | 103 | override function kill() { 104 | if (socket != null) { 105 | // the socket will get ECONNRESET and nodejs will throw if we don't handle it as an event 106 | socket.on(ReadableEvent.Error, function(_) {}); 107 | } 108 | super.kill(); 109 | } 110 | 111 | function onStderr(buf:Buffer) { 112 | lastErrorOutput = buf.toString(); 113 | logger(HaxeConnection.reTrailingNewline.replace(lastErrorOutput, "")); 114 | } 115 | 116 | override function getLastErrorOutput():String { 117 | return lastErrorOutput; 118 | } 119 | 120 | public static function start(path:String, arguments:Array, spawnOptions:ChildProcessSpawnOptions, logger:String->Void, onMessage:String->Void, 121 | onExit:HaxeConnection->Void, callback:HaxeConnection->Void) { 122 | trace("Using --server-connect"); 123 | final server = Net.createServer(); 124 | server.listen(0, function() { 125 | final port = server.address().port; 126 | final process = ChildProcess.spawn(path, arguments.concat(["--server-connect", '127.0.0.1:$port']), spawnOptions); 127 | final connection = new SocketConnection(process, logger, onMessage, onExit); 128 | server.on(ServerEvent.Connection, function(socket) { 129 | trace("Haxe connected!"); 130 | server.close(); 131 | connection.setup(socket); 132 | callback(connection); 133 | }); 134 | }); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/server/MessageBuffer.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.server; 2 | 3 | import js.node.Buffer; 4 | 5 | class MessageBuffer { 6 | static inline final DEFAULT_SIZE = 8192; 7 | 8 | var index:Int; 9 | var buffer:Buffer; 10 | 11 | public function new() { 12 | index = 0; 13 | buffer = Buffer.alloc(DEFAULT_SIZE); 14 | } 15 | 16 | public function append(chunk:Buffer):Void { 17 | if (buffer.length - index >= chunk.length) { 18 | chunk.copy(buffer, index, 0, chunk.length); 19 | } else { 20 | final newSize = (Math.ceil((index + chunk.length) / DEFAULT_SIZE) + 1) * DEFAULT_SIZE; 21 | if (index == 0) { 22 | buffer = Buffer.alloc(newSize); 23 | chunk.copy(buffer, 0, 0, chunk.length); 24 | } else { 25 | buffer = Buffer.concat([buffer.slice(0, index), chunk], newSize); 26 | } 27 | } 28 | index += chunk.length; 29 | } 30 | 31 | public function tryReadLength():Int { 32 | if (index < 4) 33 | return -1; 34 | final length = buffer.readInt32LE(0); 35 | buffer = buffer.slice(4); 36 | index -= 4; 37 | return length; 38 | } 39 | 40 | public function tryReadContent(length:Int):Null { 41 | if (index < length) 42 | return null; 43 | final result = buffer.toString("utf-8", 0, length); 44 | final nextStart = length; 45 | buffer.copy(buffer, 0, nextStart); 46 | index -= nextStart; 47 | return result; 48 | } 49 | 50 | public function getContent():String { 51 | return buffer.toString("utf-8", 0, index); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/server/ResultHandler.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.server; 2 | 3 | enum ResultHandler { 4 | /** 5 | Data is passed on as-is, with 0x01 / 0x02 chars from Haxe. 6 | Used for socket communication to ensure exit codes etc are correct. 7 | **/ 8 | Raw(callback:(result:DisplayResult) -> Void); 9 | 10 | /** 11 | Data is processed into Strings and separated 12 | into successful results (`callback`) and errors (`errback`). 13 | **/ 14 | Processed(callback:(result:DisplayResult) -> Void, errback:(error:String) -> Void); 15 | } 16 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/server/ServerRecordingTools.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.server; 2 | 3 | import haxe.io.Path; 4 | import js.lib.Promise; 5 | import js.node.Buffer; 6 | import js.node.ChildProcess; 7 | import sys.FileSystem; 8 | import sys.io.File; 9 | import haxeLanguageServer.Configuration.ServerRecordingConfig; 10 | import haxeLanguageServer.helper.FsHelper; 11 | 12 | enum VcsState { 13 | None; 14 | // Note: hasPatch will always be true for Git (for now at least) 15 | Git(ref:String, hasPatch:Bool, hasUntracked:Bool, untrackedCopy:Promise); 16 | Svn(rev:String, hasPatch:Bool); 17 | } 18 | 19 | function command(cmd:String, args:Array) { 20 | var p = ChildProcess.spawnSync(cmd, args); 21 | 22 | return { 23 | code: p.status, 24 | out: ((p.status == 0 ? p.stdout : p.stderr) : Buffer).toString().trim() 25 | }; 26 | } 27 | 28 | function getVcsState(patchOutput:String, untrackedDestination:String, config:ServerRecordingConfig):VcsState { 29 | var ret = None; 30 | ret = getGitState(patchOutput, untrackedDestination, config); 31 | if (ret.match(None)) 32 | ret = getSvnState(patchOutput, config); 33 | return ret; 34 | } 35 | 36 | function getGitState(patchOutput:String, untrackedDestination:String, config:ServerRecordingConfig):VcsState { 37 | var revision = command("git", ["rev-parse", "HEAD"]); 38 | if (revision.code != 0) 39 | return None; 40 | 41 | command("git", applyGitExcludes(["diff", "--output", patchOutput, "--patch"], config)); 42 | 43 | var hasUntracked = false; 44 | var p:Promise = Promise.resolve(); 45 | 46 | if (!config.excludeUntracked) { 47 | // Get untracked files (other than recording folder) 48 | var untracked = command("git", 49 | applyGitExcludes(["status", "--porcelain"], 50 | config)).out.split("\n") 51 | .filter(l -> l.startsWith('?? ')) 52 | .map(l -> l.substr(3)) 53 | .filter(l -> l != recordingRelativeRoot(config) && l != ".haxelib" && l != "dump"); 54 | 55 | if (untracked.length > 0) { 56 | hasUntracked = true; 57 | FileSystem.createDirectory(untrackedDestination); 58 | 59 | var promises = []; 60 | 61 | for (f in untracked) { 62 | if (f.startsWith('"')) 63 | f = f.substr(1); 64 | if (f.endsWith('"')) 65 | f = f.substr(0, f.length - 1); 66 | promises.push(FsHelper.cp(f, Path.join([untrackedDestination, f]))); 67 | } 68 | 69 | p = Promise.all(promises).then((_) -> {}); 70 | } 71 | } 72 | 73 | return Git(revision.out, true, hasUntracked, p); 74 | } 75 | 76 | function applyGitExcludes(args:Array, config:ServerRecordingConfig):Array { 77 | if (config.exclude.length == 0) 78 | return args; 79 | 80 | args.push("--"); 81 | args.push("."); 82 | for (ex in config.exclude) 83 | args.push(':^$ex'); 84 | return args; 85 | } 86 | 87 | function getSvnState(patchOutput:String, config:ServerRecordingConfig):VcsState { 88 | var revision = command("svn", ["info", "--show-item", "revision"]); 89 | if (revision.code != 0) 90 | return None; 91 | 92 | var hasExcludes = config.exclude.length > 0; 93 | var status = command("svn", ["status"]); 94 | var untracked = []; 95 | 96 | if (!config.excludeUntracked) { 97 | untracked = [ 98 | for (line in status.out.split('\n')) { 99 | if (line.charCodeAt(0) != '?'.code) 100 | continue; 101 | var entry = line.substr(1).trim(); 102 | 103 | if (hasExcludes) { 104 | var excluded = false; 105 | 106 | for (ex in config.exclude) { 107 | if (entry.startsWith(ex)) { 108 | excluded = true; 109 | break; 110 | } 111 | } 112 | 113 | if (excluded) 114 | continue; 115 | } 116 | 117 | entry; 118 | } 119 | ]; 120 | } 121 | 122 | for (f in untracked) 123 | command("svn", ["add", f]); 124 | var patch = command("svn", ["diff", "--depth=infinity", "--patch-compatible"]); 125 | var hasPatch = patch.out.trim().length > 0; 126 | if (hasPatch) 127 | File.saveContent(patchOutput, patch.out); 128 | for (f in untracked) 129 | command("svn", ["rm", "--keep-local", f]); 130 | 131 | return Svn(revision.out, hasPatch); 132 | } 133 | 134 | function recordingRelativeRoot(config:ServerRecordingConfig):String { 135 | var ret = Path.isAbsolute(config.path) ? "" : config.path; 136 | if (ret.startsWith("./") || ret.startsWith("../")) 137 | ret = ""; 138 | return ret.split("/")[0] + "/"; 139 | } 140 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/tokentree/TokenContext.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.tokentree; 2 | 3 | enum TokenContext { 4 | /** we're at root level **/ 5 | Root(pos:RootPosition); 6 | 7 | /** we're in a type **/ 8 | Type(type:TypeContext); 9 | 10 | /** we're in a module-level field **/ 11 | ModuleLevelStatic(kind:FieldKind); 12 | } 13 | 14 | enum RootPosition { 15 | BeforePackage; 16 | BeforeFirstImport; 17 | BeforeFirstType; 18 | AfterFirstType; 19 | } 20 | 21 | typedef TypeContext = { 22 | final kind:TypeKind; 23 | final ?field:{ 24 | final isStatic:Bool; 25 | final kind:FieldKind; 26 | }; 27 | } 28 | 29 | enum TypeKind { 30 | Class; 31 | Interface; 32 | Enum; 33 | EnumAbstract; 34 | Abstract; 35 | Typedef; 36 | MacroClass; // TODO: add argument for the containing type's context? 37 | } 38 | 39 | enum FieldKind { 40 | Var; 41 | Final; 42 | Function; 43 | } 44 | -------------------------------------------------------------------------------- /src/haxeLanguageServer/tokentree/TokenTreeManager.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.tokentree; 2 | 3 | import byte.ByteData; 4 | import haxe.io.Bytes; 5 | import haxe.macro.Expr.Position; 6 | import haxeparser.Data.Token; 7 | import haxeparser.HaxeLexer; 8 | import js.node.Buffer; 9 | import tokentree.TokenStream; 10 | import tokentree.TokenTree; 11 | import tokentree.TokenTreeBuilder; 12 | 13 | class TokenTreeManager { 14 | public static function create(content:String):TokenTreeManager { 15 | final bytes = Bytes.ofString(content); 16 | final tokens = createTokens(bytes); 17 | final tree = createTokenTree(bytes, tokens); 18 | return new TokenTreeManager(bytes, tokens, tree); 19 | } 20 | 21 | static function createTokens(bytes:Bytes):Array { 22 | try { 23 | final tokens = []; 24 | final lexer = new HaxeLexer(ByteData.ofBytes(bytes)); 25 | var t:Token = lexer.token(haxeparser.HaxeLexer.tok); 26 | while (t.tok != Eof) { 27 | tokens.push(t); 28 | t = lexer.token(haxeparser.HaxeLexer.tok); 29 | } 30 | return tokens; 31 | } catch (e) { 32 | throw 'failed to create tokens: $e'; 33 | } 34 | } 35 | 36 | static function createTokenTree(bytes:Bytes, tokens:Array):TokenTree { 37 | try { 38 | TokenStream.MODE = Relaxed; 39 | return TokenTreeBuilder.buildTokenTree(tokens, ByteData.ofBytes(bytes)); 40 | } catch (e) { 41 | throw 'failed to create token tree: $e'; 42 | } 43 | } 44 | 45 | public final bytes:Bytes; 46 | public final list:Array; 47 | public final tree:TokenTree; 48 | 49 | var tokenCharacterRanges:Null>; 50 | 51 | function new(bytes:Bytes, list:Array, tree:TokenTree) { 52 | this.bytes = bytes; 53 | this.list = list; 54 | this.tree = tree; 55 | } 56 | 57 | /** 58 | Gets the character position of a token. 59 | **/ 60 | public function getPos(tokenTree:TokenTree):Position { 61 | inline createTokenCharacterRanges(); 62 | final pos = tokenCharacterRanges[tokenTree.index]; 63 | return if (pos == null) tokenTree.pos else pos; 64 | } 65 | 66 | /** 67 | Gets the character position of a subtree. 68 | Copy of `TokenTree.getPos()`. 69 | **/ 70 | public function getTreePos(tokenTree:TokenTree):Position { 71 | final pos = getPos(tokenTree); 72 | final children = tokenTree.children; 73 | if (pos == null || children == null) 74 | return pos; 75 | if (children.length <= 0) 76 | return pos; 77 | 78 | final fullPos:Position = {file: pos.file, min: pos.min, max: pos.max}; 79 | for (child in children) { 80 | final childPos = getTreePos(child); 81 | if (childPos == null) 82 | continue; 83 | 84 | if (childPos.min < fullPos.min) 85 | fullPos.min = childPos.min; 86 | if (childPos.max > fullPos.max) 87 | fullPos.max = childPos.max; 88 | } 89 | return fullPos; 90 | } 91 | 92 | public function getTokenAtOffset(off:Int):Null { 93 | if (list.length <= 0) 94 | return null; 95 | 96 | if (off < 0) 97 | return null; 98 | 99 | if (off > list[list.length - 1].pos.max) 100 | return null; 101 | 102 | inline createTokenCharacterRanges(); 103 | 104 | for (index in 0...list.length) { 105 | var range = tokenCharacterRanges[index]; 106 | if (range == null) { 107 | range = list[index].pos; 108 | } 109 | if (range.max < off) 110 | continue; 111 | if (off < range.min) 112 | return null; 113 | return findTokenAtIndex(tree, index); 114 | } 115 | return null; 116 | } 117 | 118 | function findTokenAtIndex(parent:TokenTree, index:Int):Null { 119 | if (parent.children == null) { 120 | return null; 121 | } 122 | for (child in parent.children) { 123 | if (child.index == index) 124 | return child; 125 | 126 | final token:Null = findTokenAtIndex(child, index); 127 | if (token != null) 128 | return token; 129 | } 130 | return null; 131 | } 132 | 133 | function createTokenCharacterRanges() { 134 | if (tokenCharacterRanges != null) { 135 | return; 136 | } 137 | tokenCharacterRanges = new Map(); 138 | var offset = 0; 139 | for (i in 0...list.length) { 140 | final token = list[i]; 141 | offset += switch token.tok { 142 | // these should be the only places where Unicode characters can appear in Haxe 143 | case Const(CString(s)), Const(CRegexp(s, _)), Comment(s), CommentLine(s): 144 | s.length - Buffer.byteLength(s); 145 | case _: 146 | 0; 147 | } 148 | if (offset != 0) { 149 | tokenCharacterRanges[i] = { 150 | file: token.pos.file, 151 | min: token.pos.min + offset, 152 | max: token.pos.max + offset 153 | }; 154 | } 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /test/TestMain.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import haxeLanguageServer.features.haxe.codeAction.*; 4 | import haxeLanguageServer.helper.*; 5 | import haxeLanguageServer.hxParser.*; 6 | import haxeLanguageServer.protocol.*; 7 | import haxeLanguageServer.tokentree.*; 8 | import utest.UTest; 9 | 10 | class TestMain { 11 | static function main() { 12 | // @formatter:off 13 | UTest.run([ 14 | new ArrayHelperTest(), 15 | new IdentifierHelperTest(), 16 | new ImportHelperTest(), 17 | new PathHelperTest(), 18 | new PositionHelperTest(), 19 | new RangeHelperTest(), 20 | new TypeHelperTest(), 21 | new RenameResolverTest(), 22 | new ExtensionsTest(), 23 | new ExtractConstantFeatureTest(), 24 | new OrganizeImportsFeatureTest(), 25 | new SemVerTest(), 26 | new TokenTreeTest() 27 | ]); 28 | // @formatter:on 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/haxeLanguageServer/features/haxe/codeAction/ExtractConstantFeatureTest.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.features.haxe.codeAction; 2 | 3 | import haxe.Json; 4 | import haxeLanguageServer.documents.HaxeDocument; 5 | import jsonrpc.Protocol; 6 | import testcases.TestTextEditHelper; 7 | 8 | class ExtractConstantFeatureTest extends Test implements IExtractConstantFeatureCases { 9 | function goldCheck(fileName:String, input:String, gold:String, config:String) { 10 | final range:Range = Json.parse(config); 11 | if (range.end == null) 12 | range.end = {line: range.start.line, character: range.start.character}; 13 | 14 | final edits:Array = makeEdits(input, fileName, range); 15 | final goldEdit:TextDocumentEdit = Json.parse(gold); 16 | Assert.notNull(goldEdit); 17 | TestTextEditHelper.compareTextEdits(goldEdit.edits, edits); 18 | } 19 | 20 | @:access(haxeLanguageServer.features.haxe.codeAction.ExtractConstantFeature) 21 | function makeEdits(content:String, fileName:String, range:Range):Array { 22 | final context = new Context(new Protocol((_, _) -> {})); 23 | final uri = new DocumentUri("file://" + fileName + ".edittest"); 24 | final doc = new HaxeDocument(uri, "haxe", 4, content); 25 | 26 | final extractConst = new ExtractConstantFeature(context); 27 | 28 | final actions:Array = extractConst.extractConstant(doc, uri, range); 29 | Assert.equals(1, actions.length); 30 | Assert.notNull(actions[0].edit); 31 | @:nullSafety(Off) { 32 | Assert.notNull(actions[0].edit.documentChanges); 33 | Assert.equals(1, actions[0].edit.documentChanges.length); 34 | 35 | final docEdit:TextDocumentEdit = actions[0].edit.documentChanges[0]; 36 | return docEdit.edits; 37 | } 38 | } 39 | } 40 | 41 | @:autoBuild(testcases.EditTestCaseMacro.build("test/testcases/extractConstant")) 42 | private interface IExtractConstantFeatureCases {} 43 | -------------------------------------------------------------------------------- /test/haxeLanguageServer/features/haxe/codeAction/OrganizeImportsFeatureTest.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.features.haxe.codeAction; 2 | 3 | import haxe.Json; 4 | import haxeLanguageServer.Configuration; 5 | import haxeLanguageServer.documents.HaxeDocument; 6 | import jsonrpc.Protocol; 7 | import testcases.TestTextEditHelper; 8 | 9 | class OrganizeImportsFeatureTest extends Test implements IOrganizeImportsFeatureTestCases { 10 | function goldCheck(fileName:String, input:String, gold:String, config:String) { 11 | final userConfig:UserConfig = Json.parse(config); 12 | var importsSortOrder:ImportsSortOrderConfig = AllAlphabetical; 13 | if (userConfig != null && userConfig.importsSortOrder != null) 14 | importsSortOrder = userConfig.importsSortOrder; 15 | 16 | final edits:Array = makeEdits(input, fileName, importsSortOrder); 17 | final goldEdit:TextDocumentEdit = Json.parse(gold); 18 | 19 | Assert.notNull(goldEdit); 20 | TestTextEditHelper.compareTextEdits(goldEdit.edits, edits); 21 | } 22 | 23 | @:access(haxeLanguageServer.Configuration) 24 | function makeEdits(content:String, fileName:String, importsSortOrder:ImportsSortOrderConfig):Array { 25 | final context = new Context(new Protocol((_, _) -> {})); 26 | final settings = cast Reflect.copy(Configuration.DefaultUserSettings); 27 | settings.importsSortOrder = importsSortOrder; 28 | context.config.user = settings; 29 | final doc = new HaxeDocument(new DocumentUri("file://" + fileName + ".edittest"), "haxe", 4, content); 30 | return OrganizeImportsFeature.organizeImports(doc, context, []); 31 | } 32 | } 33 | 34 | @:autoBuild(testcases.EditTestCaseMacro.build("test/testcases/organizeImports")) 35 | private interface IOrganizeImportsFeatureTestCases {} 36 | -------------------------------------------------------------------------------- /test/haxeLanguageServer/helper/ArrayHelperTest.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.helper; 2 | 3 | class ArrayHelperTest extends Test { 4 | function testEquals() { 5 | Assert.isTrue([].equals([])); 6 | Assert.isTrue([1].equals([1])); 7 | 8 | Assert.isFalse([1, 2].equals([2, 1])); 9 | Assert.isFalse([1].equals([1, 1])); 10 | } 11 | 12 | function testOccurrences() { 13 | Assert.equals(0, [].occurrences("foo")); 14 | Assert.equals(1, ["foo"].occurrences("foo")); 15 | Assert.equals(2, ["bar", "foo", "bar", "bar", "foo"].occurrences("foo")); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/haxeLanguageServer/helper/IdentifierHelperTest.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.helper; 2 | 3 | import haxeLanguageServer.helper.IdentifierHelper.*; 4 | 5 | class IdentifierHelperTest extends Test { 6 | function testAvoidDuplicates() { 7 | function check(expected:Array, original:Array) 8 | Assert.same(expected, avoidDuplicates(original)); 9 | 10 | check(["a"], ["a"]); 11 | check(["b1", "b2"], ["b", "b"]); 12 | } 13 | 14 | function testGuessName() { 15 | function assert(expected, original, ?posInfos) 16 | Assert.equals(expected, guessName(original), posInfos); 17 | 18 | assert("object", "FlxObject"); 19 | assert("f", "F"); 20 | assert("params", "CodeActionParams"); 21 | assert("case", "PascalCase"); 22 | assert("int64", "__Int64"); 23 | assert("f", "Float"); 24 | assert("b", "Bool"); 25 | assert("i", "Null>"); 26 | assert("s", "String"); 27 | assert("d", "Dynamic"); 28 | assert("n", "Null"); 29 | assert("t", "True"); 30 | assert("f", "False"); 31 | assert("unknown", null); 32 | assert("c", "C"); 33 | assert("unknown", ""); 34 | assert("struct", "{}"); 35 | assert("struct", "{ i : Int }"); 36 | assert("t", "method.T"); 37 | assert("event", "foo.bar.SomeEvent"); 38 | assert("_", "Void"); 39 | assert("_", "Null"); 40 | } 41 | 42 | function testAddNamesToSignatureType() { 43 | function assert(expected, original, ?posInfos) 44 | Assert.equals(expected, addNamesToSignatureType(original), posInfos); 45 | 46 | function assertUnchanged(expectedAndOriginal, ?posInfos) 47 | assert(expectedAndOriginal, expectedAndOriginal, posInfos); 48 | 49 | assertUnchanged("String"); 50 | 51 | assert("a:{ i : Int }", ":{ i : Int }"); 52 | assert("a:{ i : Int, s : String }", ":{ i : Int, s : String }"); 53 | assert("a:{}", ":{}"); 54 | assert("(a:Int, b:{ s : String, i : Int }):Void", "(:Int, :{ s : String, i : Int }):Void"); 55 | 56 | assert("a:Int", ":Int"); 57 | assertUnchanged("a:Int"); 58 | 59 | assert("(a:Int, b:Int):Void", "(:Int, :Int):Void"); 60 | assert("(?a:Int, ?b:Int):Void", "(?:Int, ?:Int):Void"); 61 | assertUnchanged("(a:Int, b:Int):Void"); 62 | assert("(a:Int, b:Int, c:Int):Void", "(:Int, :Int,:Int) : Void"); 63 | 64 | assertUnchanged("("); 65 | assertUnchanged("():haxe.ds.Option"); 66 | assertUnchanged("():haxe.__Int64"); 67 | assertUnchanged("():Array"); 68 | assertUnchanged("():{ a:Int, b:Bool }"); 69 | 70 | // hopefully this is never needed... 71 | final letterOverflow = '(${[for (_ in 0...30) ":Int"].join(", ")}):Void'; 72 | final fixedSignature = addNamesToSignatureType(letterOverflow); 73 | Assert.isFalse(fixedSignature.contains("{:")); // { comes after z in ascii 74 | Assert.equals(2, fixedSignature.split(" b:").length); // arg names must be unique 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/haxeLanguageServer/helper/ImportHelperTest.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.helper; 2 | 3 | import haxe.PosInfos; 4 | import haxeLanguageServer.documents.HaxeDocument; 5 | 6 | class ImportHelperTest extends Test { 7 | function testGetImportInsertPosition() { 8 | function test(testCase:{before:String, after:String}, ?pos:PosInfos) { 9 | testCase.before = testCase.before.replace("\r", ""); 10 | testCase.after = testCase.after.replace("\r", ""); 11 | 12 | final doc = new HaxeDocument(new DocumentUri("file://dummy"), "", 0, testCase.before); 13 | final result = ImportHelper.determineImportPosition(doc); 14 | final edit = ImportHelper.createImportsEdit(doc, result, ["Test"], Type); 15 | 16 | // TODO: apply the edit properly instead of this hack? 17 | final lines = testCase.before.split("\n"); 18 | final insertLine = edit.range.start.line; 19 | lines[insertLine] = edit.newText + lines[insertLine]; 20 | 21 | Assert.equals(testCase.after, lines.join("\n"), pos); 22 | } 23 | 24 | // package + import 25 | test({ 26 | before: "package; 27 | 28 | import haxe.io.Path;", 29 | 30 | after: "package; 31 | 32 | import Test; 33 | import haxe.io.Path;" 34 | }); 35 | 36 | // package + import with conditional compilation 37 | test({ 38 | before: "package; 39 | 40 | #if false 41 | import haxe.io.Path; 42 | #end", 43 | 44 | after: "package; 45 | 46 | import Test; 47 | #if false 48 | import haxe.io.Path; 49 | #end" 50 | }); 51 | 52 | // only import 53 | test({ 54 | before: " 55 | 56 | import haxe.io.Path;", 57 | 58 | after: " 59 | 60 | import Test; 61 | import haxe.io.Path;" 62 | }); 63 | 64 | // only type 65 | test({ 66 | before: "class Foo { 67 | }", 68 | after: "import Test; 69 | 70 | class Foo { 71 | }" 72 | }); 73 | 74 | // type with empty line 75 | test({ 76 | before: " 77 | class Foo { 78 | }", 79 | 80 | after: "import Test; 81 | 82 | class Foo { 83 | }" 84 | }); 85 | 86 | // License header 87 | test({ 88 | before: " 89 | /** 90 | License 91 | **/ 92 | 93 | import Foo;", 94 | 95 | after: " 96 | /** 97 | License 98 | **/ 99 | 100 | import Test; 101 | import Foo;" 102 | }); 103 | 104 | // doc comment and license header 105 | test({ 106 | before: " 107 | /** 108 | License 109 | **/ 110 | 111 | /** 112 | Docs 113 | **/ 114 | class Foo", 115 | 116 | after: " 117 | /** 118 | License 119 | **/ 120 | 121 | import Test; 122 | 123 | /** 124 | Docs 125 | **/ 126 | class Foo" 127 | }); 128 | 129 | // doc comment, line comments and license header 130 | test({ 131 | before: " 132 | /** 133 | License 134 | **/ 135 | 136 | // TODO 137 | // TODO 2 138 | /** 139 | Docs 140 | **/ 141 | class Foo", 142 | 143 | after: " 144 | /** 145 | License 146 | **/ 147 | 148 | import Test; 149 | 150 | // TODO 151 | // TODO 2 152 | /** 153 | Docs 154 | **/ 155 | class Foo" 156 | }); 157 | 158 | test({ 159 | before: " 160 | package; 161 | 162 | @:jsRequire('WebSocket') 163 | extern class WebSocket {}", 164 | 165 | after: " 166 | package; 167 | 168 | import Test; 169 | 170 | @:jsRequire('WebSocket') 171 | extern class WebSocket {}", 172 | 173 | }); 174 | 175 | // issue #414 https://github.com/vshaxe/vshaxe/issues/414 176 | // first import + meta with comment 177 | test({ 178 | before: " 179 | package; 180 | 181 | @:keep // comment seems to affect this bug 182 | class Main { 183 | static function main() { 184 | 185 | } 186 | }", 187 | 188 | after: " 189 | package; 190 | 191 | import Test; 192 | 193 | @:keep // comment seems to affect this bug 194 | class Main { 195 | static function main() { 196 | 197 | } 198 | }", 199 | 200 | }); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /test/haxeLanguageServer/helper/PathHelperTest.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.helper; 2 | 3 | import haxe.PosInfos; 4 | 5 | class PathHelperTest extends Test { 6 | public function testMatches() { 7 | function matches(filter:TestFilter, path:TestPath):Bool { 8 | final pathFilter = PathHelper.preparePathFilter(filter, TestPath.HaxelibPath, TestPath.WorkspaceRoot); 9 | return PathHelper.matches(path, pathFilter); 10 | } 11 | function match(filter:TestFilter, path:TestPath, ?pos:PosInfos) { 12 | Assert.isTrue(matches(filter, path), pos); 13 | } 14 | function fail(filter:TestFilter, path:TestPath, ?pos:PosInfos) { 15 | Assert.isFalse(matches(filter, path), pos); 16 | } 17 | 18 | match(WorkspaceRoot, WorkspaceRoot); 19 | match(WorkspaceSource, WorkspaceSource); 20 | fail(WorkspaceSource, WorkspaceExport); 21 | 22 | match(HaxelibPath, HaxelibPath); 23 | match(Flixel, FlxG); 24 | match(FlixelOrLime, FlxG); 25 | match(FlixelOrLime, LimeSystem); 26 | fail(FlixelOrLime, Hscript); 27 | 28 | match(MatchAll, WorkspaceRoot); 29 | match(MatchAll, WorkspaceSource); 30 | match(MatchAll, WorkspaceExport); 31 | match(MatchAll, HaxelibPath); 32 | match(MatchAll, FlxG); 33 | match(MatchAll, LimeSystem); 34 | match(MatchAll, Hscript); 35 | match(MatchAll, LinuxPath); 36 | } 37 | 38 | public function testNormalize() { 39 | function test(expected:String, path:String, ?pos:PosInfos) { 40 | Assert.equals(expected, PathHelper.normalize(new FsPath(path)).toString(), pos); 41 | } 42 | 43 | test("c:/HaxeToolkit/haxe", "C:\\HaxeToolkit\\haxe"); 44 | test("c:/HaxeToolkit/haxe", "c:/HaxeToolkit/haxe"); 45 | test("/usr/bin", "/usr/bin"); 46 | } 47 | } 48 | 49 | enum abstract TestFilter(String) to String { 50 | final WorkspaceRoot = "${workspaceRoot}"; 51 | final WorkspaceSource = WorkspaceRoot + "/source"; 52 | final HaxelibPath = "${haxelibPath}"; 53 | final Flixel = HaxelibPath + "\\flixel"; 54 | final FlixelOrLime = HaxelibPath + "/(flixel|lime)"; 55 | final MatchAll = ".*?"; 56 | } 57 | 58 | enum abstract TestPath(String) to String { 59 | final WorkspaceRoot = "c:/projects/vshaxe"; 60 | final WorkspaceSource = WorkspaceRoot + "/source"; 61 | final WorkspaceExport = WorkspaceRoot + "/export"; 62 | final HaxelibPath = "C:\\HaxeToolkit\\haxe\\lib"; 63 | final FlxG = HaxelibPath + "/flixel/git/flixel/FlxG.hx"; 64 | final LimeSystem = HaxelibPath + "/lime/2,9,1/lime/system/System.hx"; 65 | final Hscript = HaxelibPath + "/hscript/2,0,7/hscript/"; 66 | final LinuxPath = "~/../../../lib/"; 67 | 68 | @:to function toFsPath():FsPath 69 | return new FsPath(this); 70 | } 71 | -------------------------------------------------------------------------------- /test/haxeLanguageServer/helper/PositionHelperTest.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.helper; 2 | 3 | class PositionHelperTest extends Test { 4 | function testPositionsEqual() { 5 | function check(l1, c1, l2, c2) 6 | return {line: l1, character: c1}.isEqual({line: l2, character: c2}); 7 | Assert.isTrue(check(0, 10, 0, 10)); 8 | Assert.isFalse(check(1, 5, 5, 1)); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/haxeLanguageServer/helper/RangeHelperTest.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.helper; 2 | 3 | class RangeHelperTest extends Test { 4 | function testRangeIsEmpty() { 5 | function check(l1, c1, l2, c2) 6 | return {start: {line: l1, character: c1}, end: {line: l2, character: c2}}.isEmpty(); 7 | Assert.isTrue(check(0, 10, 0, 10)); 8 | Assert.isFalse(check(1, 5, 5, 1)); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/haxeLanguageServer/helper/SemVerTest.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.helper; 2 | 3 | class SemVerTest extends Test { 4 | @:nullSafety(Off) 5 | function spec() { 6 | SemVer.parse("3.4.7") == new SemVer(3, 4, 7); 7 | SemVer.parse("4.0.0 (git build master @ 2344f233a)") == new SemVer(4, 0, 0); 8 | 9 | SemVer.parse("4.0.0-rc.1+1fdd3d59b") == new SemVer(4, 0, 0); 10 | SemVer.parse("4.0.0-rc.1+1fdd3d59b").pre == "rc.1"; 11 | SemVer.parse("4.0.0-rc.1+1fdd3d59b").build == "1fdd3d59b"; 12 | 13 | SemVer.parse("4.0.0+ef18b2627e") == new SemVer(4, 0, 0); 14 | SemVer.parse("4.0.0+ef18b2627e").pre == null; 15 | SemVer.parse("4.0.0+ef18b2627e").build == "ef18b2627e"; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/haxeLanguageServer/helper/TypeHelperTest.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.helper; 2 | 3 | import haxeLanguageServer.Configuration.FunctionFormattingConfig; 4 | import haxeLanguageServer.helper.TypeHelper.*; 5 | import haxeLanguageServer.helper.TypeHelper; 6 | 7 | class TypeHelperTest extends Test { 8 | function testParseFunctionArgumentType() { 9 | final parsed = parseFunctionArgumentType("?Callback:Null ?String -> Void>"); 10 | switch parsed { 11 | case DisplayType.DTFunction(args, ret): 12 | Assert.equals("flixel.FlxObject", args[0].type); 13 | Assert.isNull(args[0].opt); 14 | Assert.equals("String", args[1].type); 15 | Assert.notNull(args[1].opt); 16 | Assert.isTrue(args[1].opt ?? false); 17 | Assert.equals(2, args.length); 18 | Assert.equals("Void", ret); 19 | case _: 20 | Assert.fail(); 21 | } 22 | } 23 | 24 | function testParseFunctionArgumentTypeNestedNulls() { 25 | final parsed = parseFunctionArgumentType("foo:Null Void>>>>"); 26 | switch parsed { 27 | case DisplayType.DTFunction(args, ret): 28 | Assert.equals("String", args[0].type); 29 | Assert.equals(1, args.length); 30 | Assert.equals("Void", ret); 31 | case _: 32 | Assert.fail(); 33 | } 34 | } 35 | 36 | function testPrintFunctionDeclaration() { 37 | assertPrintedEquals(parseFunctionArgumentType, "function(a:flixel.FlxObject, ?b:String):Void", "?Callback:Null ?String -> Void>", 38 | {argumentTypeHints: true, returnTypeHint: Always, useArrowSyntax: false}); 39 | 40 | assertPrintedEquals(parseDisplayType, "function(a, b)", "String -> Bool -> Void>", 41 | {argumentTypeHints: false, returnTypeHint: Never, useArrowSyntax: false}); 42 | 43 | assertPrintedEquals(parseDisplayType, "function(a:String, b:Bool)", "String -> Bool -> Void", 44 | {argumentTypeHints: true, returnTypeHint: NonVoid, useArrowSyntax: false}); 45 | 46 | assertPrintedEquals(parseDisplayType, "function():String", "Void -> String", 47 | {argumentTypeHints: true, returnTypeHint: NonVoid, useArrowSyntax: false}); 48 | } 49 | 50 | function testPrintArrowFunctionDeclaration() { 51 | function assert(expected, functionType, argumentTypeHints = false) { 52 | assertPrintedEquals(parseDisplayType, expected, functionType, 53 | {argumentTypeHints: argumentTypeHints, returnTypeHint: Always, useArrowSyntax: true}); 54 | } 55 | 56 | assert("() ->", "Void -> Void"); 57 | assert("() ->", "Void -> String"); 58 | assert("a ->", "String -> Void"); 59 | assert("(a:String) ->", "String -> Void", true); 60 | assert("(a:String, b:Bool) ->", "String -> Bool -> Void", true); 61 | } 62 | 63 | function assertPrintedEquals(parser:String->Null, expected:String, functionType:String, formatting) { 64 | final parsed = parseFunctionArgumentType(functionType); 65 | switch parsed { 66 | case DisplayType.DTFunction(args, ret): 67 | final decl = printFunctionDeclaration(args, ret, makeConfig(formatting)); 68 | Assert.equals(expected, decl); 69 | case _: 70 | Assert.fail(); 71 | } 72 | } 73 | 74 | function makeConfig(config):FunctionFormattingConfig { 75 | return { 76 | argumentTypeHints: config.argumentTypeHints, 77 | returnTypeHint: config.returnTypeHint, 78 | useArrowSyntax: config.useArrowSyntax, 79 | placeOpenBraceOnNewLine: false, 80 | explicitPublic: false, 81 | explicitPrivate: false, 82 | explicitNull: false 83 | }; 84 | } 85 | 86 | function testGetModule() { 87 | Assert.equals("Module", getModule("Module")); 88 | Assert.equals("Module", getModule("Module.Type")); 89 | 90 | Assert.equals("foo.bar.Module", getModule("foo.bar.Module")); 91 | Assert.equals("foo.bar.Module", getModule("foo.bar.Module.Type")); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /test/haxeLanguageServer/protocol/ExtensionsTest.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.protocol; 2 | 3 | import haxe.display.JsonModuleTypes; 4 | 5 | using Lambda; 6 | 7 | class ExtensionsTest extends Test { 8 | function testResolveImports() { 9 | final imports = Extensions.resolveImports({ 10 | kind: TInst, 11 | args: { 12 | path: { 13 | typeName: "Vector", 14 | pack: ["haxe", "ds"], 15 | moduleName: "Vector", 16 | importStatus: Unimported 17 | }, 18 | params: [ 19 | { 20 | kind: TInst, 21 | args: { 22 | path: { 23 | typeName: "PosInfos", 24 | pack: ["haxe"], 25 | moduleName: "PosInfos", 26 | importStatus: Unimported 27 | } 28 | } 29 | } 30 | ] 31 | } 32 | }); 33 | Assert.equals(2, imports.length); 34 | Assert.isTrue(imports.exists(path -> path.typeName == "PosInfos")); 35 | Assert.isTrue(imports.exists(path -> path.typeName == "Vector")); 36 | } 37 | 38 | function testRemoveNulls() { 39 | final result = Extensions.removeNulls({ 40 | "kind": TAbstract, 41 | "args": { 42 | "path": { 43 | "typeName": "Null", 44 | "moduleName": "StdTypes", 45 | "pack": [] 46 | }, 47 | "params": [ 48 | { 49 | "kind": TInst, 50 | "args": { 51 | "path": { 52 | "typeName": "String", 53 | "moduleName": "String", 54 | "pack": [] 55 | }, 56 | "params": [] 57 | } 58 | } 59 | ] 60 | } 61 | }); 62 | Assert.isTrue(result.nullable); 63 | Assert.equals("String", result.type.args.path.typeName); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/haxeLanguageServer/tokentree/TokenTreeTest.hx: -------------------------------------------------------------------------------- 1 | package haxeLanguageServer.tokentree; 2 | 3 | import haxe.Json; 4 | import haxe.PosInfos; 5 | import haxeLanguageServer.documents.HaxeDocument; 6 | import haxeLanguageServer.features.haxe.documentSymbols.DocumentSymbolsResolver; 7 | import haxeLanguageServer.features.haxe.foldingRange.FoldingRangeResolver; 8 | 9 | class TokenTreeTest extends Test { 10 | function testDocumentSymbols() { 11 | compareOutput("cases/documentSymbols", document -> { 12 | return new DocumentSymbolsResolver(document).resolve(); 13 | }); 14 | } 15 | 16 | function testFoldingRange() { 17 | compareOutput("cases/foldingRange", document -> { 18 | return new FoldingRangeResolver(document, {foldingRange: {lineFoldingOnly: false}}).resolve(); 19 | }); 20 | } 21 | 22 | function compareOutput(basePath:String, resolve:(document:HaxeDocument) -> Null, ?pos:PosInfos) { 23 | final inputPath = '$basePath/Input.hx'; 24 | 25 | var content = sys.io.File.getContent(inputPath); 26 | content = content.replace("\r", ""); 27 | final uri = new FsPath(inputPath).toUri(); 28 | final document = new HaxeDocument(uri, "haxe", 0, content); 29 | 30 | final stringify = Json.stringify.bind(_, null, "\t"); 31 | final actual = stringify(resolve(document)); 32 | sys.io.File.saveContent('$basePath/Actual.json', actual); 33 | final expected = stringify(Json.parse(sys.io.File.getContent('$basePath/Expected.json'))); 34 | 35 | // use "Compare Active File With..." and select Actual.json and Expected.json for debugging 36 | Assert.equals(expected, actual, pos); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/import.hx: -------------------------------------------------------------------------------- 1 | import haxe.display.FsPath; 2 | import languageServerProtocol.Types; 3 | import languageServerProtocol.protocol.Protocol; 4 | import languageServerProtocol.textdocument.TextDocument; 5 | import utest.Assert; 6 | import utest.Test; 7 | 8 | using StringTools; 9 | using haxeLanguageServer.extensions.ArrayExtensions; 10 | using haxeLanguageServer.extensions.DocumentUriExtensions; 11 | using haxeLanguageServer.extensions.FsPathExtensions; 12 | using haxeLanguageServer.extensions.PositionExtensions; 13 | using haxeLanguageServer.extensions.RangeExtensions; 14 | using haxeLanguageServer.extensions.StringExtensions; 15 | -------------------------------------------------------------------------------- /test/testcases/EditTestCaseMacro.hx: -------------------------------------------------------------------------------- 1 | package testcases; 2 | 3 | import haxe.io.Path; 4 | import haxe.macro.Context; 5 | import haxe.macro.Expr; 6 | import sys.FileSystem; 7 | import sys.io.File; 8 | 9 | class EditTestCaseMacro { 10 | #if macro 11 | public macro static function build(folder:String):Array { 12 | final fields:Array = Context.getBuildFields(); 13 | final testCases:Array = collectAllFileNames(folder); 14 | for (testCase in testCases) { 15 | final field:Field = buildTestCaseField(testCase); 16 | if (field == null) 17 | continue; 18 | 19 | fields.push(field); 20 | } 21 | return fields; 22 | } 23 | 24 | static function buildTestCaseField(fileName:String):Field { 25 | Context.registerModuleDependency(Context.getLocalModule(), fileName); 26 | final content:String = sys.io.File.getContent(fileName); 27 | final nl = "\r?\n"; 28 | final reg = new EReg('$nl$nl---$nl$nl', "g"); 29 | final segments = reg.split(content); 30 | if (segments.length != 3) 31 | throw 'invalid testcase format for: $fileName'; 32 | 33 | final config:String = segments[0]; 34 | final input:String = segments[1]; 35 | final gold:String = segments[2]; 36 | final fileName:String = new haxe.io.Path(fileName).file; 37 | var fieldName:String = fileName; 38 | fieldName = "test" + fieldName.charAt(0).toUpperCase() + fieldName.substr(1); 39 | 40 | return (macro class { 41 | @Test 42 | public function $fieldName() { 43 | goldCheck($v{fileName}, $v{input}, $v{gold}, $v{config}); 44 | }; 45 | }).fields[0]; 46 | } 47 | 48 | static function collectAllFileNames(path:String):Array { 49 | #if display 50 | return []; 51 | #end 52 | final items:Array = FileSystem.readDirectory(path); 53 | var files:Array = []; 54 | for (item in items) { 55 | if (item == "." || item == "..") 56 | continue; 57 | 58 | final fileName = Path.join([path, item]); 59 | if (FileSystem.isDirectory(fileName)) { 60 | files = files.concat(collectAllFileNames(fileName)); 61 | continue; 62 | } 63 | if (!item.endsWith(".edittest")) 64 | continue; 65 | 66 | files.push(Path.join([path, item])); 67 | } 68 | return files; 69 | } 70 | #end 71 | } 72 | -------------------------------------------------------------------------------- /test/testcases/TestTextEditHelper.hx: -------------------------------------------------------------------------------- 1 | package testcases; 2 | 3 | class TestTextEditHelper { 4 | public static function compareTextEdits(goldEdits:Array, actualEdits:Array) { 5 | Assert.notNull(actualEdits); 6 | Assert.notNull(goldEdits); 7 | Assert.equals(goldEdits.length, actualEdits.length); 8 | for (index in 0...goldEdits.length) { 9 | final expectedEdit:TextEdit = goldEdits[index]; 10 | final actualEdit:TextEdit = actualEdits[index]; 11 | Assert.equals(expectedEdit.newText, actualEdit.newText); 12 | if (expectedEdit.range != null) { 13 | Assert.equals(expectedEdit.range.start.line, actualEdit.range.start.line); 14 | Assert.equals(expectedEdit.range.start.character, actualEdit.range.start.character); 15 | Assert.equals(expectedEdit.range.end.line, actualEdit.range.end.line); 16 | Assert.equals(expectedEdit.range.end.character, actualEdit.range.end.character); 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/testcases/extractConstant/ExtractConstant_FILE.edittest: -------------------------------------------------------------------------------- 1 | { 2 | "start": { 3 | "line": 3, 4 | "character": 41 5 | } 6 | } 7 | 8 | --- 9 | 10 | class Main { 11 | function makeEdits(content:String, fileName:String, range:Range):Array { 12 | var context:Context = new Context(new Protocol(null)); 13 | var uri:DocumentUri = new DocumentUri("file://" + fileName + ".edittest"); 14 | var doc = new TextDocument(context, uri, "haxe", 4, content); 15 | 16 | var extractConst:ExtractConstantFeature = new ExtractConstantFeature(context); 17 | 18 | var actions:Array = extractConst.internalExtractConstant(doc, uri, range); 19 | Assert.equals(1, actions.length); 20 | 21 | var docEdit:TextDocumentEdit = cast actions[0].edit.documentChanges[0]; 22 | return docEdit.edits; 23 | } 24 | } 25 | 26 | --- 27 | 28 | { 29 | "edits": [{ 30 | "newText": "static inline final FILE = \"file://\";\n\n\t" 31 | },{ 32 | "newText": "FILE" 33 | }] 34 | } 35 | -------------------------------------------------------------------------------- /test/testcases/extractConstant/ExtractConstant_HAXE.edittest: -------------------------------------------------------------------------------- 1 | { 2 | "start": { 3 | "line": 4, 4 | "character": 44 5 | } 6 | } 7 | 8 | --- 9 | 10 | class Main { 11 | function makeEdits(content:String, fileName:String, range:Range):Array { 12 | var context:Context = new Context(new Protocol(null)); 13 | var uri:DocumentUri = new DocumentUri("file://" + fileName + ".edittest"); 14 | var doc = new TextDocument(context, uri, "haxe", 4, content); 15 | 16 | var extractConst:ExtractConstantFeature = new ExtractConstantFeature(context); 17 | 18 | var actions:Array = extractConst.internalExtractConstant(doc, uri, range); 19 | Assert.equals(1, actions.length); 20 | 21 | var docEdit:TextDocumentEdit = cast actions[0].edit.documentChanges[0]; 22 | return docEdit.edits; 23 | } 24 | } 25 | 26 | --- 27 | 28 | { 29 | "edits": [{ 30 | "newText": "static inline final HAXE = \"haxe\";\n\n\t" 31 | },{ 32 | "newText": "HAXE" 33 | }] 34 | } 35 | -------------------------------------------------------------------------------- /test/testcases/extractConstant/ExtractConstant_HAXE_singlequote.edittest: -------------------------------------------------------------------------------- 1 | { 2 | "start": { 3 | "line": 4, 4 | "character": 44 5 | } 6 | } 7 | 8 | --- 9 | 10 | class Main { 11 | function makeEdits(content:String, fileName:String, range:Range):Array { 12 | var context:Context = new Context(new Protocol(null)); 13 | var uri:DocumentUri = new DocumentUri("file://" + fileName + ".edittest"); 14 | var doc = new TextDocument(context, uri, 'haxe', 4, content); 15 | 16 | var extractConst:ExtractConstantFeature = new ExtractConstantFeature(context); 17 | 18 | var actions:Array = extractConst.internalExtractConstant(doc, uri, range); 19 | Assert.equals(1, actions.length); 20 | 21 | var docEdit:TextDocumentEdit = cast actions[0].edit.documentChanges[0]; 22 | return docEdit.edits; 23 | } 24 | } 25 | 26 | --- 27 | 28 | { 29 | "edits": [{ 30 | "newText": "static inline final HAXE = 'haxe';\n\n\t" 31 | },{ 32 | "newText": "HAXE" 33 | }] 34 | } 35 | -------------------------------------------------------------------------------- /test/testcases/extractConstant/ExtractConstant_multiple.edittest: -------------------------------------------------------------------------------- 1 | { 2 | "start": { 3 | "line": 2, 4 | "character": 36 5 | } 6 | } 7 | 8 | --- 9 | 10 | class Main { 11 | function init() { 12 | new JQuery ("#id1").removeClass ("disabled"); 13 | new JQuery ("#id1").prop ("readonly", false); 14 | 15 | new JQuery ("#id2").removeClass ("disabled"); 16 | new JQuery ("#id2").prop ("readonly", false); 17 | } 18 | } 19 | 20 | --- 21 | 22 | { 23 | "edits": [{ 24 | "newText": "static inline final DISABLED = \"disabled\";\n\n\t" 25 | },{ 26 | "newText": "DISABLED" 27 | },{ 28 | "newText": "DISABLED" 29 | }] 30 | } 31 | -------------------------------------------------------------------------------- /test/testcases/extractConstant/ExtractConstant_umlaut_begin.edittest: -------------------------------------------------------------------------------- 1 | { 2 | "start": { 3 | "line": 6, 4 | "character": 36 5 | } 6 | } 7 | 8 | --- 9 | 10 | // öäüßł€µ”„¢«»æſðđŋħ̣ĸħĸĸ̣ 11 | class Main { 12 | function init() { 13 | new JQuery ("#id1").removeClass ("disabled"); 14 | new JQuery ("#id1").prop ("readonly", false); 15 | // öäüßł€µ”„¢«»æſðđŋħ̣ĸħĸĸ̣ 16 | new JQuery ("#id2").removeClass ("disabled"); 17 | new JQuery ("#id2").prop ("readonly", false); 18 | } 19 | } 20 | 21 | --- 22 | 23 | { 24 | "edits": [{ 25 | "newText": "static inline final DISABLED = \"disabled\";\n\n\t" 26 | },{ 27 | "newText": "DISABLED" 28 | },{ 29 | "newText": "DISABLED" 30 | }] 31 | } 32 | -------------------------------------------------------------------------------- /test/testcases/extractConstant/ExtractConstant_umlaut_end.edittest: -------------------------------------------------------------------------------- 1 | { 2 | "start": { 3 | "line": 6, 4 | "character": 44 5 | } 6 | } 7 | 8 | --- 9 | 10 | // öäüßł€µ”„¢«»æſðđŋħ̣ĸħĸĸ̣ 11 | class Main { 12 | function init() { 13 | new JQuery ("#id1").removeClass ("disabled"); 14 | new JQuery ("#id1").prop ("readonly", false); 15 | // öäüßł€µ”„¢«»æſðđŋħ̣ĸħĸĸ̣ 16 | new JQuery ("#id2").removeClass ("disabled"); 17 | new JQuery ("#id2").prop ("readonly", false); 18 | } 19 | } 20 | 21 | --- 22 | 23 | { 24 | "edits": [{ 25 | "newText": "static inline final DISABLED = \"disabled\";\n\n\t" 26 | },{ 27 | "newText": "DISABLED" 28 | },{ 29 | "newText": "DISABLED" 30 | }] 31 | } 32 | -------------------------------------------------------------------------------- /test/testcases/organizeImports/ConditionalImportsWithPackage.edittest: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | 4 | --- 5 | 6 | // öäüßł€µ”„¢«»æſðđŋħ̣ĸħĸĸ̣ 7 | package haxeLanguageServer.features; 8 | 9 | import haxeLanguageServer.helper.FormatterHelper; 10 | import tokentree.TokenTreeBuilder; 11 | import haxe.ds.ArraySort; 12 | import tokentree.TokenTree; 13 | import formatter.Main; 14 | import haxeLanguageServer.helper.WorkspaceEditHelper; 15 | 16 | #if js 17 | import haxeLanguageServer.helper.FormatterHelper; 18 | import tokentree.TokenTreeBuilder; 19 | import haxe.ds.ArraySort; 20 | import tokentree.TokenTree; 21 | import formatter.Main; 22 | import haxeLanguageServer.helper.WorkspaceEditHelper; 23 | #else 24 | import haxeLanguageServer.helper.FormatterHelper; 25 | import tokentree.TokenTreeBuilder; 26 | import haxe.ds.ArraySort; 27 | import tokentree.TokenTree; 28 | import formatter.Main; 29 | import haxeLanguageServer.helper.WorkspaceEditHelper; 30 | #end 31 | 32 | --- 33 | 34 | { 35 | "edits": [{ 36 | "newText": "" 37 | },{ 38 | "newText": "" 39 | },{ 40 | "newText": "" 41 | },{ 42 | "newText": "" 43 | },{ 44 | "newText": "" 45 | },{ 46 | "newText": "" 47 | },{ 48 | "newText": "import formatter.Main;\nimport haxe.ds.ArraySort;\nimport haxeLanguageServer.helper.FormatterHelper;\nimport haxeLanguageServer.helper.WorkspaceEditHelper;\nimport tokentree.TokenTree;\nimport tokentree.TokenTreeBuilder;\n" 49 | },{ 50 | "newText": "" 51 | },{ 52 | "newText": "" 53 | },{ 54 | "newText": "" 55 | },{ 56 | "newText": "" 57 | },{ 58 | "newText": "" 59 | },{ 60 | "newText": "" 61 | },{ 62 | "newText": "import formatter.Main;\nimport haxe.ds.ArraySort;\nimport haxeLanguageServer.helper.FormatterHelper;\nimport haxeLanguageServer.helper.WorkspaceEditHelper;\nimport tokentree.TokenTree;\nimport tokentree.TokenTreeBuilder;\n" 63 | },{ 64 | "newText": "" 65 | },{ 66 | "newText": "" 67 | },{ 68 | "newText": "" 69 | },{ 70 | "newText": "" 71 | },{ 72 | "newText": "" 73 | },{ 74 | "newText": "" 75 | },{ 76 | "range": { 77 | "start": { "line": 3, "character": 0 }, 78 | "end": { "line": 3, "character": 0 } 79 | }, 80 | "newText": "import formatter.Main;\nimport haxe.ds.ArraySort;\nimport haxeLanguageServer.helper.FormatterHelper;\nimport haxeLanguageServer.helper.WorkspaceEditHelper;\nimport tokentree.TokenTree;\nimport tokentree.TokenTreeBuilder;\n" 81 | }] 82 | } 83 | -------------------------------------------------------------------------------- /test/testcases/organizeImports/ConditionalImportsWithPackage_AA.edittest: -------------------------------------------------------------------------------- 1 | { 2 | "importsSortOrder": "all-alphabetical" 3 | } 4 | 5 | --- 6 | 7 | // öäüßł€µ”„¢«»æſðđŋħ̣ĸħĸĸ̣ 8 | package haxeLanguageServer.features; 9 | 10 | import haxeLanguageServer.helper.FormatterHelper; 11 | import tokentree.TokenTreeBuilder; 12 | import haxe.ds.ArraySort; 13 | import tokentree.TokenTree; 14 | import formatter.Main; 15 | import haxeLanguageServer.helper.WorkspaceEditHelper; 16 | 17 | #if js 18 | import haxeLanguageServer.helper.FormatterHelper; 19 | import tokentree.TokenTreeBuilder; 20 | import haxe.ds.ArraySort; 21 | import tokentree.TokenTree; 22 | import formatter.Main; 23 | import haxeLanguageServer.helper.WorkspaceEditHelper; 24 | #else 25 | import haxeLanguageServer.helper.FormatterHelper; 26 | import tokentree.TokenTreeBuilder; 27 | import haxe.ds.ArraySort; 28 | import tokentree.TokenTree; 29 | import formatter.Main; 30 | import haxeLanguageServer.helper.WorkspaceEditHelper; 31 | #end 32 | 33 | --- 34 | 35 | { 36 | "edits": [{ 37 | "newText": "" 38 | },{ 39 | "newText": "" 40 | },{ 41 | "newText": "" 42 | },{ 43 | "newText": "" 44 | },{ 45 | "newText": "" 46 | },{ 47 | "newText": "" 48 | },{ 49 | "newText": "import formatter.Main;\nimport haxe.ds.ArraySort;\nimport haxeLanguageServer.helper.FormatterHelper;\nimport haxeLanguageServer.helper.WorkspaceEditHelper;\nimport tokentree.TokenTree;\nimport tokentree.TokenTreeBuilder;\n" 50 | },{ 51 | "newText": "" 52 | },{ 53 | "newText": "" 54 | },{ 55 | "newText": "" 56 | },{ 57 | "newText": "" 58 | },{ 59 | "newText": "" 60 | },{ 61 | "newText": "" 62 | },{ 63 | "newText": "import formatter.Main;\nimport haxe.ds.ArraySort;\nimport haxeLanguageServer.helper.FormatterHelper;\nimport haxeLanguageServer.helper.WorkspaceEditHelper;\nimport tokentree.TokenTree;\nimport tokentree.TokenTreeBuilder;\n" 64 | },{ 65 | "newText": "" 66 | },{ 67 | "newText": "" 68 | },{ 69 | "newText": "" 70 | },{ 71 | "newText": "" 72 | },{ 73 | "newText": "" 74 | },{ 75 | "newText": "" 76 | },{ 77 | "range": { 78 | "start": { "line": 3, "character": 0 }, 79 | "end": { "line": 3, "character": 0 } 80 | }, 81 | "newText": "import formatter.Main;\nimport haxe.ds.ArraySort;\nimport haxeLanguageServer.helper.FormatterHelper;\nimport haxeLanguageServer.helper.WorkspaceEditHelper;\nimport tokentree.TokenTree;\nimport tokentree.TokenTreeBuilder;\n" 82 | }] 83 | } 84 | -------------------------------------------------------------------------------- /test/testcases/organizeImports/ConditionalImportsWithPackage_NPP.edittest: -------------------------------------------------------------------------------- 1 | { 2 | "importsSortOrder": "non-project -> project" 3 | } 4 | 5 | --- 6 | 7 | // öäüßł€µ”„¢«»æſðđŋħ̣ĸħĸĸ̣ 8 | package haxeLanguageServer.features; 9 | 10 | import haxeLanguageServer.helper.FormatterHelper; 11 | import tokentree.TokenTreeBuilder; 12 | import haxe.ds.ArraySort; 13 | import tokentree.TokenTree; 14 | import formatter.Main; 15 | import haxeLanguageServer.helper.WorkspaceEditHelper; 16 | 17 | #if js 18 | import haxeLanguageServer.helper.FormatterHelper; 19 | import tokentree.TokenTreeBuilder; 20 | import haxe.ds.ArraySort; 21 | import tokentree.TokenTree; 22 | import formatter.Main; 23 | import haxeLanguageServer.helper.WorkspaceEditHelper; 24 | #else 25 | import haxeLanguageServer.helper.FormatterHelper; 26 | import tokentree.TokenTreeBuilder; 27 | import haxe.ds.ArraySort; 28 | import tokentree.TokenTree; 29 | import formatter.Main; 30 | import haxeLanguageServer.helper.WorkspaceEditHelper; 31 | #end 32 | 33 | --- 34 | 35 | { 36 | "edits": [{ 37 | "newText": "" 38 | },{ 39 | "newText": "" 40 | },{ 41 | "newText": "" 42 | },{ 43 | "newText": "" 44 | },{ 45 | "newText": "" 46 | },{ 47 | "newText": "" 48 | },{ 49 | "newText": "import formatter.Main;\nimport haxe.ds.ArraySort;\nimport tokentree.TokenTree;\nimport tokentree.TokenTreeBuilder;\nimport haxeLanguageServer.helper.FormatterHelper;\nimport haxeLanguageServer.helper.WorkspaceEditHelper;\n" 50 | },{ 51 | "newText": "" 52 | },{ 53 | "newText": "" 54 | },{ 55 | "newText": "" 56 | },{ 57 | "newText": "" 58 | },{ 59 | "newText": "" 60 | },{ 61 | "newText": "" 62 | },{ 63 | "newText": "import formatter.Main;\nimport haxe.ds.ArraySort;\nimport tokentree.TokenTree;\nimport tokentree.TokenTreeBuilder;\nimport haxeLanguageServer.helper.FormatterHelper;\nimport haxeLanguageServer.helper.WorkspaceEditHelper;\n" 64 | },{ 65 | "newText": "" 66 | },{ 67 | "newText": "" 68 | },{ 69 | "newText": "" 70 | },{ 71 | "newText": "" 72 | },{ 73 | "newText": "" 74 | },{ 75 | "newText": "" 76 | },{ 77 | "range": { 78 | "start": { "line": 3, "character": 0 }, 79 | "end": { "line": 3, "character": 0 } 80 | }, 81 | "newText": "import formatter.Main;\nimport haxe.ds.ArraySort;\nimport tokentree.TokenTree;\nimport tokentree.TokenTreeBuilder;\nimport haxeLanguageServer.helper.FormatterHelper;\nimport haxeLanguageServer.helper.WorkspaceEditHelper;\n" 82 | }] 83 | } 84 | -------------------------------------------------------------------------------- /test/testcases/organizeImports/ConditionalImportsWithPackage_SLP.edittest: -------------------------------------------------------------------------------- 1 | { 2 | "importsSortOrder": "stdlib -> libs -> project" 3 | } 4 | 5 | --- 6 | 7 | // öäüßł€µ”„¢«»æſðđŋħ̣ĸħĸĸ̣ 8 | package haxeLanguageServer.features; 9 | 10 | import haxeLanguageServer.helper.FormatterHelper; 11 | import tokentree.TokenTreeBuilder; 12 | import haxe.ds.ArraySort; 13 | import tokentree.TokenTree; 14 | import formatter.Main; 15 | import haxeLanguageServer.helper.WorkspaceEditHelper; 16 | 17 | #if js 18 | import haxeLanguageServer.helper.FormatterHelper; 19 | import tokentree.TokenTreeBuilder; 20 | import haxe.ds.ArraySort; 21 | import tokentree.TokenTree; 22 | import formatter.Main; 23 | import haxeLanguageServer.helper.WorkspaceEditHelper; 24 | #else 25 | import haxeLanguageServer.helper.FormatterHelper; 26 | import tokentree.TokenTreeBuilder; 27 | import haxe.ds.ArraySort; 28 | import tokentree.TokenTree; 29 | import formatter.Main; 30 | import haxeLanguageServer.helper.WorkspaceEditHelper; 31 | #end 32 | 33 | --- 34 | 35 | { 36 | "edits": [{ 37 | "newText": "" 38 | },{ 39 | "newText": "" 40 | },{ 41 | "newText": "" 42 | },{ 43 | "newText": "" 44 | },{ 45 | "newText": "" 46 | },{ 47 | "newText": "" 48 | },{ 49 | "newText": "import haxe.ds.ArraySort;\nimport formatter.Main;\nimport tokentree.TokenTree;\nimport tokentree.TokenTreeBuilder;\nimport haxeLanguageServer.helper.FormatterHelper;\nimport haxeLanguageServer.helper.WorkspaceEditHelper;\n" 50 | },{ 51 | "newText": "" 52 | },{ 53 | "newText": "" 54 | },{ 55 | "newText": "" 56 | },{ 57 | "newText": "" 58 | },{ 59 | "newText": "" 60 | },{ 61 | "newText": "" 62 | },{ 63 | "newText": "import haxe.ds.ArraySort;\nimport formatter.Main;\nimport tokentree.TokenTree;\nimport tokentree.TokenTreeBuilder;\nimport haxeLanguageServer.helper.FormatterHelper;\nimport haxeLanguageServer.helper.WorkspaceEditHelper;\n" 64 | },{ 65 | "newText": "" 66 | },{ 67 | "newText": "" 68 | },{ 69 | "newText": "" 70 | },{ 71 | "newText": "" 72 | },{ 73 | "newText": "" 74 | },{ 75 | "newText": "" 76 | },{ 77 | "range": { 78 | "start": { "line": 3, "character": 0 }, 79 | "end": { "line": 3, "character": 0 } 80 | }, 81 | "newText": "import haxe.ds.ArraySort;\nimport formatter.Main;\nimport tokentree.TokenTree;\nimport tokentree.TokenTreeBuilder;\nimport haxeLanguageServer.helper.FormatterHelper;\nimport haxeLanguageServer.helper.WorkspaceEditHelper;\n" 82 | }] 83 | } 84 | -------------------------------------------------------------------------------- /test/testcases/organizeImports/ImportsWithClass.edittest: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | 4 | --- 5 | 6 | package haxeLanguageServer.features; 7 | 8 | import haxeLanguageServer.helper.FormatterHelper; 9 | import tokentree.TokenTreeBuilder; 10 | import haxe.ds.ArraySort; 11 | import tokentree.TokenTree; 12 | import formatter.Main; 13 | import haxeLanguageServer.helper.WorkspaceEditHelper; 14 | 15 | class Main {} 16 | 17 | --- 18 | 19 | { 20 | "edits": [{ 21 | "newText": "" 22 | },{ 23 | "newText": "" 24 | },{ 25 | "newText": "" 26 | },{ 27 | "newText": "" 28 | },{ 29 | "newText": "" 30 | },{ 31 | "newText": "" 32 | },{ 33 | "range": { 34 | "start": { "line": 2, "character": 0 }, 35 | "end": { "line": 2, "character": 0 } 36 | }, 37 | "newText": "import formatter.Main;\nimport haxe.ds.ArraySort;\nimport haxeLanguageServer.helper.FormatterHelper;\nimport haxeLanguageServer.helper.WorkspaceEditHelper;\nimport tokentree.TokenTree;\nimport tokentree.TokenTreeBuilder;\n" 38 | }] 39 | } 40 | -------------------------------------------------------------------------------- /test/testcases/organizeImports/ImportsWithClassWithCommentedOutImport.edittest: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | 4 | --- 5 | 6 | package haxeLanguageServer.features; 7 | 8 | import haxeLanguageServer.helper.FormatterHelper; 9 | import tokentree.TokenTreeBuilder; 10 | import haxe.ds.ArraySort; 11 | // import tokentree.TokenTree; 12 | import formatter.Main; 13 | import haxeLanguageServer.helper.WorkspaceEditHelper; 14 | 15 | class Main {} 16 | 17 | --- 18 | 19 | { 20 | "edits": [{ 21 | "newText": "" 22 | },{ 23 | "newText": "" 24 | },{ 25 | "newText": "" 26 | },{ 27 | "newText": "" 28 | },{ 29 | "newText": "" 30 | },{ 31 | "newText": "" 32 | },{ 33 | "range": { 34 | "start": { "line": 2, "character": 0 }, 35 | "end": { "line": 2, "character": 0 } 36 | }, 37 | "newText": "import formatter.Main;\nimport haxe.ds.ArraySort;\nimport haxeLanguageServer.helper.FormatterHelper;\nimport haxeLanguageServer.helper.WorkspaceEditHelper;\n// import tokentree.TokenTree;\nimport tokentree.TokenTreeBuilder;\n" 38 | }] 39 | } 40 | -------------------------------------------------------------------------------- /test/testcases/organizeImports/ImportsWithClassWithSpaces.edittest: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | 4 | --- 5 | 6 | package haxeLanguageServer.features; 7 | 8 | import haxeLanguageServer.helper.FormatterHelper; 9 | 10 | import tokentree.TokenTreeBuilder; 11 | import haxe.ds.ArraySort; 12 | 13 | import tokentree.TokenTree; 14 | import formatter.Main; 15 | 16 | import haxeLanguageServer.helper.WorkspaceEditHelper; 17 | 18 | class Main {} 19 | 20 | --- 21 | 22 | { 23 | "edits": [{ 24 | "newText": "" 25 | },{ 26 | "newText": "" 27 | },{ 28 | "newText": "" 29 | },{ 30 | "newText": "" 31 | },{ 32 | "newText": "" 33 | },{ 34 | "newText": "" 35 | },{ 36 | "range": { 37 | "start": { "line": 2, "character": 0 }, 38 | "end": { "line": 2, "character": 0 } 39 | }, 40 | "newText": "import formatter.Main;\nimport haxe.ds.ArraySort;\nimport haxeLanguageServer.helper.FormatterHelper;\nimport haxeLanguageServer.helper.WorkspaceEditHelper;\nimport tokentree.TokenTree;\nimport tokentree.TokenTreeBuilder;\n" 41 | }] 42 | } 43 | -------------------------------------------------------------------------------- /test/testcases/organizeImports/ImportsWithClassWithSpacesAndUsing.edittest: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | 4 | --- 5 | 6 | package haxeLanguageServer.features; 7 | 8 | import haxeLanguageServer.helper.FormatterHelper; 9 | 10 | import tokentree.TokenTreeBuilder; 11 | import haxe.ds.ArraySort; 12 | 13 | import tokentree.TokenTree; 14 | import formatter.Main; 15 | 16 | using haxe.Json; 17 | 18 | import haxeLanguageServer.helper.WorkspaceEditHelper; 19 | 20 | class Main {} 21 | 22 | --- 23 | 24 | { 25 | "edits": [{ 26 | "newText": "" 27 | },{ 28 | "newText": "" 29 | },{ 30 | "newText": "" 31 | },{ 32 | "newText": "" 33 | },{ 34 | "newText": "" 35 | },{ 36 | "newText": "" 37 | },{ 38 | "newText": "" 39 | },{ 40 | "range": { 41 | "start": { "line": 2, "character": 0 }, 42 | "end": { "line": 2, "character": 0 } 43 | }, 44 | "newText": "import formatter.Main;\nimport haxe.ds.ArraySort;\nimport haxeLanguageServer.helper.FormatterHelper;\nimport haxeLanguageServer.helper.WorkspaceEditHelper;\nimport tokentree.TokenTree;\nimport tokentree.TokenTreeBuilder;\n\nusing haxe.Json;\n" 45 | }] 46 | } 47 | -------------------------------------------------------------------------------- /test/testcases/organizeImports/ImportsWithClass_conditional.edittest: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | 4 | --- 5 | 6 | package haxeLanguageServer.features; 7 | 8 | import haxeLanguageServer.helper.FormatterHelper; 9 | import tokentree.TokenTreeBuilder; 10 | import haxe.ds.ArraySort; 11 | import tokentree.TokenTree; 12 | import formatter.Main; 13 | import haxeLanguageServer.helper.WorkspaceEditHelper; 14 | 15 | #if something 16 | import haxe.Json; 17 | #end 18 | 19 | class Main {} 20 | 21 | --- 22 | 23 | { 24 | "edits": [{ 25 | "newText": "" 26 | },{ 27 | "range": { 28 | "start": { "line": 10, "character": 0 }, 29 | "end": { "line": 10, "character": 0 } 30 | }, 31 | "newText": "import haxe.Json;\n" 32 | },{ 33 | "newText": "" 34 | },{ 35 | "newText": "" 36 | },{ 37 | "newText": "" 38 | },{ 39 | "newText": "" 40 | },{ 41 | "newText": "" 42 | },{ 43 | "newText": "" 44 | },{ 45 | "range": { 46 | "start": { "line": 2, "character": 0 }, 47 | "end": { "line": 2, "character": 0 } 48 | }, 49 | "newText": "import formatter.Main;\nimport haxe.ds.ArraySort;\nimport haxeLanguageServer.helper.FormatterHelper;\nimport haxeLanguageServer.helper.WorkspaceEditHelper;\nimport tokentree.TokenTree;\nimport tokentree.TokenTreeBuilder;\n" 50 | }] 51 | } 52 | -------------------------------------------------------------------------------- /test/testcases/organizeImports/ImportsWithClass_conditional_first.edittest: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | 4 | --- 5 | 6 | package haxeLanguageServer.features; 7 | 8 | #if something 9 | import haxe.Json; 10 | #end 11 | 12 | import haxeLanguageServer.helper.FormatterHelper; 13 | import tokentree.TokenTreeBuilder; 14 | import haxe.ds.ArraySort; 15 | import tokentree.TokenTree; 16 | import formatter.Main; 17 | import haxeLanguageServer.helper.WorkspaceEditHelper; 18 | 19 | class Main {} 20 | 21 | --- 22 | 23 | { 24 | "edits": [{ 25 | "newText": "" 26 | },{ 27 | "range": { 28 | "start": { "line": 3, "character": 0 }, 29 | "end": { "line": 3, "character": 0 } 30 | }, 31 | "newText": "import haxe.Json;\n" 32 | },{ 33 | "newText": "" 34 | },{ 35 | "newText": "" 36 | },{ 37 | "newText": "" 38 | },{ 39 | "newText": "" 40 | },{ 41 | "newText": "" 42 | },{ 43 | "newText": "" 44 | },{ 45 | "range": { 46 | "start": { "line": 6, "character": 0 }, 47 | "end": { "line": 6, "character": 0 } 48 | }, 49 | "newText": "import formatter.Main;\nimport haxe.ds.ArraySort;\nimport haxeLanguageServer.helper.FormatterHelper;\nimport haxeLanguageServer.helper.WorkspaceEditHelper;\nimport tokentree.TokenTree;\nimport tokentree.TokenTreeBuilder;\n" 50 | }] 51 | } 52 | -------------------------------------------------------------------------------- /test/testcases/organizeImports/ImportsWithPackage.edittest: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | 4 | --- 5 | 6 | // öäüßł€µ”„¢«»æſðđŋħ̣ĸħĸĸ̣ 7 | package haxeLanguageServer.features; 8 | 9 | import haxeLanguageServer.helper.FormatterHelper; 10 | import tokentree.TokenTreeBuilder; 11 | import haxe.ds.ArraySort; 12 | import tokentree.TokenTree; 13 | import formatter.Main; 14 | import haxeLanguageServer.helper.WorkspaceEditHelper; 15 | 16 | --- 17 | 18 | { 19 | "edits": [{ 20 | "newText": "" 21 | },{ 22 | "newText": "" 23 | },{ 24 | "newText": "" 25 | },{ 26 | "newText": "" 27 | },{ 28 | "newText": "" 29 | },{ 30 | "newText": "" 31 | },{ 32 | "range": { 33 | "start": { "line": 3, "character": 0 }, 34 | "end": { "line": 3, "character": 0 } 35 | }, 36 | "newText": "import formatter.Main;\nimport haxe.ds.ArraySort;\nimport haxeLanguageServer.helper.FormatterHelper;\nimport haxeLanguageServer.helper.WorkspaceEditHelper;\nimport tokentree.TokenTree;\nimport tokentree.TokenTreeBuilder;\n" 37 | }] 38 | } 39 | -------------------------------------------------------------------------------- /test/testcases/organizeImports/ImportsWithPackage_AA.edittest: -------------------------------------------------------------------------------- 1 | { 2 | "importsSortOrder": "all-alphabetical" 3 | } 4 | 5 | --- 6 | 7 | // öäüßł€µ”„¢«»æſðđŋħ̣ĸħĸĸ̣ 8 | package haxeLanguageServer.features; 9 | 10 | import haxeLanguageServer.helper.FormatterHelper; 11 | import tokentree.TokenTreeBuilder; 12 | import haxe.ds.ArraySort; 13 | import tokentree.TokenTree; 14 | import formatter.Main; 15 | import haxeLanguageServer.helper.WorkspaceEditHelper; 16 | 17 | --- 18 | 19 | { 20 | "edits": [{ 21 | "newText": "" 22 | },{ 23 | "newText": "" 24 | },{ 25 | "newText": "" 26 | },{ 27 | "newText": "" 28 | },{ 29 | "newText": "" 30 | },{ 31 | "newText": "" 32 | },{ 33 | "range": { 34 | "start": { "line": 3, "character": 0 }, 35 | "end": { "line": 3, "character": 0 } 36 | }, 37 | "newText": "import formatter.Main;\nimport haxe.ds.ArraySort;\nimport haxeLanguageServer.helper.FormatterHelper;\nimport haxeLanguageServer.helper.WorkspaceEditHelper;\nimport tokentree.TokenTree;\nimport tokentree.TokenTreeBuilder;\n" 38 | }] 39 | } 40 | -------------------------------------------------------------------------------- /test/testcases/organizeImports/ImportsWithPackage_NPP.edittest: -------------------------------------------------------------------------------- 1 | { 2 | "importsSortOrder": "non-project -> project" 3 | } 4 | 5 | --- 6 | 7 | // öäüßł€µ”„¢«»æſðđŋħ̣ĸħĸĸ̣ 8 | package haxeLanguageServer.features; 9 | 10 | import haxeLanguageServer.helper.FormatterHelper; 11 | import tokentree.TokenTreeBuilder; 12 | import haxe.ds.ArraySort; 13 | import tokentree.TokenTree; 14 | import formatter.Main; 15 | import haxeLanguageServer.helper.WorkspaceEditHelper; 16 | 17 | --- 18 | 19 | { 20 | "edits": [{ 21 | "newText": "" 22 | },{ 23 | "newText": "" 24 | },{ 25 | "newText": "" 26 | },{ 27 | "newText": "" 28 | },{ 29 | "newText": "" 30 | },{ 31 | "newText": "" 32 | },{ 33 | "range": { 34 | "start": { "line": 3, "character": 0 }, 35 | "end": { "line": 3, "character": 0 } 36 | }, 37 | "newText": "import formatter.Main;\nimport haxe.ds.ArraySort;\nimport tokentree.TokenTree;\nimport tokentree.TokenTreeBuilder;\nimport haxeLanguageServer.helper.FormatterHelper;\nimport haxeLanguageServer.helper.WorkspaceEditHelper;\n" 38 | }] 39 | } 40 | -------------------------------------------------------------------------------- /test/testcases/organizeImports/ImportsWithPackage_SLP.edittest: -------------------------------------------------------------------------------- 1 | { 2 | "importsSortOrder": "stdlib -> libs -> project" 3 | } 4 | 5 | --- 6 | 7 | // öäüßł€µ”„¢«»æſðđŋħ̣ĸħĸĸ̣ 8 | package haxeLanguageServer.features; 9 | 10 | import haxeLanguageServer.helper.FormatterHelper; 11 | import tokentree.TokenTreeBuilder; 12 | import haxe.ds.ArraySort; 13 | import tokentree.TokenTree; 14 | import formatter.Main; 15 | import haxeLanguageServer.helper.WorkspaceEditHelper; 16 | 17 | --- 18 | 19 | { 20 | "edits": [{ 21 | "newText": "" 22 | },{ 23 | "newText": "" 24 | },{ 25 | "newText": "" 26 | },{ 27 | "newText": "" 28 | },{ 29 | "newText": "" 30 | },{ 31 | "newText": "" 32 | },{ 33 | "range": { 34 | "start": { "line": 3, "character": 0 }, 35 | "end": { "line": 3, "character": 0 } 36 | }, 37 | "newText": "import haxe.ds.ArraySort;\nimport formatter.Main;\nimport tokentree.TokenTree;\nimport tokentree.TokenTreeBuilder;\nimport haxeLanguageServer.helper.FormatterHelper;\nimport haxeLanguageServer.helper.WorkspaceEditHelper;\n" 38 | }] 39 | } 40 | -------------------------------------------------------------------------------- /test/testcases/organizeImports/ImportsWithoutPackage.edittest: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | 4 | --- 5 | 6 | import haxeLanguageServer.helper.FormatterHelper; 7 | import tokentree.TokenTreeBuilder; 8 | import haxe.ds.ArraySort; 9 | import tokentree.TokenTree; 10 | import formatter.Main; 11 | import haxeLanguageServer.helper.WorkspaceEditHelper; 12 | 13 | --- 14 | 15 | { 16 | "edits": [{ 17 | "newText": "" 18 | },{ 19 | "newText": "" 20 | },{ 21 | "newText": "" 22 | },{ 23 | "newText": "" 24 | },{ 25 | "newText": "" 26 | },{ 27 | "newText": "" 28 | },{ 29 | "newText": "import formatter.Main;\nimport haxe.ds.ArraySort;\nimport haxeLanguageServer.helper.FormatterHelper;\nimport haxeLanguageServer.helper.WorkspaceEditHelper;\nimport tokentree.TokenTree;\nimport tokentree.TokenTreeBuilder;\n" 30 | }] 31 | } 32 | -------------------------------------------------------------------------------- /test/testcases/organizeImports/ImportsWithoutPackage_AA.edittest: -------------------------------------------------------------------------------- 1 | { 2 | "importsSortOrder": "all-alphabetical" 3 | } 4 | 5 | --- 6 | 7 | import haxeLanguageServer.helper.FormatterHelper; 8 | import tokentree.TokenTreeBuilder; 9 | import haxe.ds.ArraySort; 10 | import tokentree.TokenTree; 11 | import formatter.Main; 12 | import haxeLanguageServer.helper.WorkspaceEditHelper; 13 | 14 | --- 15 | 16 | { 17 | "edits": [{ 18 | "newText": "" 19 | },{ 20 | "newText": "" 21 | },{ 22 | "newText": "" 23 | },{ 24 | "newText": "" 25 | },{ 26 | "newText": "" 27 | },{ 28 | "newText": "" 29 | },{ 30 | "newText": "import formatter.Main;\nimport haxe.ds.ArraySort;\nimport haxeLanguageServer.helper.FormatterHelper;\nimport haxeLanguageServer.helper.WorkspaceEditHelper;\nimport tokentree.TokenTree;\nimport tokentree.TokenTreeBuilder;\n" 31 | }] 32 | } 33 | -------------------------------------------------------------------------------- /test/testcases/organizeImports/ImportsWithoutPackage_NPP.edittest: -------------------------------------------------------------------------------- 1 | { 2 | "importsSortOrder": "non-project -> project" 3 | } 4 | 5 | --- 6 | 7 | import haxeLanguageServer.helper.FormatterHelper; 8 | import tokentree.TokenTreeBuilder; 9 | import haxe.ds.ArraySort; 10 | import tokentree.TokenTree; 11 | import formatter.Main; 12 | import haxeLanguageServer.helper.WorkspaceEditHelper; 13 | 14 | --- 15 | 16 | { 17 | "edits": [{ 18 | "newText": "" 19 | },{ 20 | "newText": "" 21 | },{ 22 | "newText": "" 23 | },{ 24 | "newText": "" 25 | },{ 26 | "newText": "" 27 | },{ 28 | "newText": "" 29 | },{ 30 | "newText": "import haxe.ds.ArraySort;\nimport formatter.Main;\nimport haxeLanguageServer.helper.FormatterHelper;\nimport haxeLanguageServer.helper.WorkspaceEditHelper;\nimport tokentree.TokenTree;\nimport tokentree.TokenTreeBuilder;\n" 31 | }] 32 | } 33 | -------------------------------------------------------------------------------- /test/testcases/organizeImports/ImportsWithoutPackage_SLP.edittest: -------------------------------------------------------------------------------- 1 | { 2 | "importsSortOrder": "stdlib -> libs -> project" 3 | } 4 | 5 | --- 6 | 7 | import haxeLanguageServer.helper.FormatterHelper; 8 | import tokentree.TokenTreeBuilder; 9 | import haxe.ds.ArraySort; 10 | import tokentree.TokenTree; 11 | import formatter.Main; 12 | import haxeLanguageServer.helper.WorkspaceEditHelper; 13 | 14 | --- 15 | 16 | { 17 | "edits": [{ 18 | "newText": "" 19 | },{ 20 | "newText": "" 21 | },{ 22 | "newText": "" 23 | },{ 24 | "newText": "" 25 | },{ 26 | "newText": "" 27 | },{ 28 | "newText": "" 29 | },{ 30 | "newText": "import haxe.ds.ArraySort;\nimport formatter.Main;\nimport haxeLanguageServer.helper.FormatterHelper;\nimport haxeLanguageServer.helper.WorkspaceEditHelper;\nimport tokentree.TokenTree;\nimport tokentree.TokenTreeBuilder;\n" 31 | }] 32 | } 33 | -------------------------------------------------------------------------------- /vshaxe-build.json: -------------------------------------------------------------------------------- 1 | { 2 | "inherit": "vshaxe-node", 3 | "mainTarget": "language-server", 4 | "targets": [ 5 | { 6 | "name": "language-server", 7 | "args": { 8 | "haxelibs": [ 9 | "haxe-hxparser", 10 | "hxparse", 11 | "haxeparser", 12 | "tokentree", 13 | "formatter", 14 | "rename", 15 | "json2object", 16 | "language-server-protocol", 17 | "vscode-json-rpc", 18 | "uglifyjs", 19 | "safety" 20 | ], 21 | "classPaths": [ 22 | "src", 23 | "shared" 24 | ], 25 | "defines": [ 26 | "JSTACK_FORMAT=vscode", 27 | "uglifyjs_overwrite" 28 | ], 29 | "output": { 30 | "target": "js", 31 | "path": "bin/server.js" 32 | }, 33 | "macros": [ 34 | "haxeLanguageServer.Init.run()", 35 | "Safety.safeNavigation('haxeLanguageServer')", 36 | "nullSafety('haxeLanguageServer')" 37 | ], 38 | "deadCodeElimination": "full", 39 | "main": "haxeLanguageServer.Main" 40 | } 41 | }, 42 | { 43 | "name": "language-server-tests", 44 | "args": { 45 | "haxelibs": [ 46 | "haxe-hxparser", 47 | "hxparse", 48 | "haxeparser", 49 | "tokentree", 50 | "formatter", 51 | "rename", 52 | "json2object", 53 | "language-server-protocol", 54 | "vscode-json-rpc", 55 | "safety", 56 | "utest" 57 | ], 58 | "classPaths": [ 59 | "src", 60 | "shared", 61 | "test" 62 | ], 63 | "output": { 64 | "target": "js", 65 | "path": "bin/test.js" 66 | }, 67 | "macros": [ 68 | "Safety.safeNavigation('haxeLanguageServer')" 69 | ], 70 | "deadCodeElimination": "full", 71 | "main": "TestMain", 72 | "debug": true 73 | }, 74 | "afterBuildCommands": [ 75 | ["node", "bin/test.js"] 76 | ] 77 | } 78 | ] 79 | } --------------------------------------------------------------------------------