├── .cargo └── config.toml ├── .github └── ISSUE_TEMPLATE │ ├── 1-grammar-bug.yml │ ├── 2-language-server-bug.yml │ ├── 3-other-bug.yml │ └── config.yml ├── .gitignore ├── Cargo.toml ├── README.md ├── extension.toml ├── languages ├── java │ ├── brackets.scm │ ├── config.toml │ ├── folds.scm │ ├── highlights.scm │ ├── indents.scm │ ├── injections.scm │ ├── locals.scm │ └── outline.scm └── properties │ ├── config.toml │ └── highlights.scm └── src └── lib.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-wasip1" 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-grammar-bug.yml: -------------------------------------------------------------------------------- 1 | name: Grammar Bug 2 | description: A bug related to the Tree-Sitter grammar (e.g. syntax highlighting, auto-indents, outline, bracket-closing). 3 | labels: ["bug", "grammar"] 4 | type: "Bug" 5 | body: 6 | - type: checkboxes 7 | id: relevance-confirmation 8 | attributes: 9 | label: Relevance Confirmation 10 | description: Have you confirmed by checking the Tree-Sitter Java grammar first that this is indeed a bug with the Zed Java extension? 11 | options: 12 | - label: I confirmed that this is not an issue with the Tree-Sitter Java grammar itself 13 | required: true 14 | - type: textarea 15 | id: what-happened 16 | attributes: 17 | label: What happened? 18 | description: Describe your bug encounter. 19 | placeholder: After doing A, B, and then C, I saw X, Y, and Z. 20 | validations: 21 | required: true 22 | - type: textarea 23 | id: expected 24 | attributes: 25 | label: What did you expect to happen? 26 | placeholder: I expected to see 1, 2, and 3. 27 | validations: 28 | required: true 29 | - type: textarea 30 | id: environment 31 | attributes: 32 | label: Environment 33 | value: | 34 | Zed: 35 | Platform: 36 | validations: 37 | required: true 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-language-server-bug.yml: -------------------------------------------------------------------------------- 1 | name: Language Server Bug 2 | description: A bug related to the language server (e.g. autocomplete, diagnostics, hover-docs, go to symbol, initialization options). 3 | labels: ["bug", "language-server"] 4 | type: "Bug" 5 | body: 6 | - type: checkboxes 7 | id: relevance-confirmation 8 | attributes: 9 | label: Relevance Confirmation 10 | description: Have you confirmed by checking Eclipse JDT Language Server first that this is indeed a bug with the Zed Java extension? 11 | options: 12 | - label: I confirmed that this is not an issue with the Eclipse JDT Language Server itself 13 | required: true 14 | - type: textarea 15 | id: what-happened 16 | attributes: 17 | label: What happened? 18 | description: Describe your bug encounter. 19 | placeholder: After doing A, B, and then C, I saw X, Y, and Z. 20 | validations: 21 | required: true 22 | - type: textarea 23 | id: expected 24 | attributes: 25 | label: What did you expect to happen? 26 | placeholder: I expected to see 1, 2, and 3. 27 | validations: 28 | required: true 29 | - type: textarea 30 | id: environment 31 | attributes: 32 | label: Environment 33 | value: | 34 | Zed: 35 | Platform: 36 | Java: 37 | validations: 38 | required: true 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3-other-bug.yml: -------------------------------------------------------------------------------- 1 | name: Other Bug 2 | description: A bug related to something else! 3 | labels: ["bug"] 4 | type: "Bug" 5 | body: 6 | - type: textarea 7 | id: what-happened 8 | attributes: 9 | label: What happened? 10 | description: Describe your bug encounter. 11 | placeholder: After doing A, B, and then C, I saw X, Y, and Z. 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: expected 16 | attributes: 17 | label: What did you expect to happen? 18 | placeholder: I expected to see 1, 2, and 3. 19 | validations: 20 | required: true 21 | - type: textarea 22 | id: environment 23 | attributes: 24 | label: Environment 25 | value: | 26 | Zed: 27 | Platform: 28 | validations: 29 | required: true 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Tree-Sitter Java 4 | url: https://github.com/tree-sitter/tree-sitter-java/issues 5 | about: Please create issues related to the grammar here first. 6 | - name: Eclipse JDT Language Server 7 | url: https://github.com/eclipse-jdtls/eclipse.jdt.ls/issues 8 | about: Please create issues related to the language server here first. 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | grammars/ 2 | target/ 3 | Cargo.lock 4 | extension.wasm 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "java" 3 | version = "6.0.4" 4 | edition = "2024" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | [dependencies] 10 | zed_extension_api = "0.3.0" 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zed Java 2 | 3 | This extension adds support for the Java language. 4 | 5 | ## Configuration Options 6 | 7 | If [Lombok] support is enabled via [JDTLS] configuration option 8 | (`settings.java.jdt.ls.lombokSupport.enabled`), this 9 | extension will download and add [Lombok] as a javaagent to the JVM arguments for 10 | [JDTLS]. 11 | 12 | There are also many more options you can pass directly to the language server, 13 | for example: 14 | 15 | ```jsonc 16 | { 17 | "lsp": { 18 | "jdtls": { 19 | "initialization_options": { 20 | "bundles": [], 21 | "workspaceFolders": ["file:///home/snjeza/Project"], 22 | }, 23 | "settings": { 24 | "java": { 25 | "home": "/usr/local/jdk-9.0.1", 26 | "errors": { 27 | "incompleteClasspath": { 28 | "severity": "warning", 29 | }, 30 | }, 31 | "configuration": { 32 | "updateBuildConfiguration": "interactive", 33 | "maven": { 34 | "userSettings": null, 35 | }, 36 | }, 37 | "trace": { 38 | "server": "verbose", 39 | }, 40 | "import": { 41 | "gradle": { 42 | "enabled": true, 43 | }, 44 | "maven": { 45 | "enabled": true, 46 | }, 47 | "exclusions": [ 48 | "**/node_modules/**", 49 | "**/.metadata/**", 50 | "**/archetype-resources/**", 51 | "**/META-INF/maven/**", 52 | "/**/test/**", 53 | ], 54 | }, 55 | "jdt": { 56 | "ls": { 57 | "lombokSupport": { 58 | "enabled": false, // Set this to true to enable lombok support 59 | }, 60 | }, 61 | }, 62 | "referencesCodeLens": { 63 | "enabled": false, 64 | }, 65 | "signatureHelp": { 66 | "enabled": false, 67 | }, 68 | "implementationsCodeLens": { 69 | "enabled": false, 70 | }, 71 | "format": { 72 | "enabled": true, 73 | }, 74 | "saveActions": { 75 | "organizeImports": false, 76 | }, 77 | "contentProvider": { 78 | "preferred": null, 79 | }, 80 | "autobuild": { 81 | "enabled": false, 82 | }, 83 | "completion": { 84 | "favoriteStaticMembers": [ 85 | "org.junit.Assert.*", 86 | "org.junit.Assume.*", 87 | "org.junit.jupiter.api.Assertions.*", 88 | "org.junit.jupiter.api.Assumptions.*", 89 | "org.junit.jupiter.api.DynamicContainer.*", 90 | "org.junit.jupiter.api.DynamicTest.*", 91 | ], 92 | "importOrder": ["java", "javax", "com", "org"], 93 | }, 94 | }, 95 | }, 96 | }, 97 | }, 98 | } 99 | ``` 100 | 101 | *Example taken from JDTLS's [configuration options wiki page].* 102 | 103 | You can see all the options JDTLS accepts [here][configuration options wiki page]. 104 | 105 | [JDTLS]: https://github.com/eclipse-jdtls/eclipse.jdt.ls 106 | [configuration options wiki page]: https://github.com/eclipse-jdtls/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line#initialize-request 107 | [Lombok]: https://projectlombok.org 108 | -------------------------------------------------------------------------------- /extension.toml: -------------------------------------------------------------------------------- 1 | id = "java" 2 | name = "Java" 3 | version = "6.0.4" 4 | schema_version = 1 5 | authors = [ 6 | "Valentine Briese ", 7 | "Samuser107 L.Longheval ", 8 | "Yury Abykhodau ", 9 | ] 10 | description = "Java support." 11 | repository = "https://github.com/zed-extensions/java" 12 | 13 | [grammars.java] 14 | repository = "https://github.com/tree-sitter/tree-sitter-java" 15 | commit = "94703d5a6bed02b98e438d7cad1136c01a60ba2c" 16 | 17 | [grammars.properties] 18 | repository = "https://github.com/tree-sitter-grammars/tree-sitter-properties" 19 | commit = "579b62f5ad8d96c2bb331f07d1408c92767531d9" 20 | 21 | [language_servers.jdtls] 22 | name = "Eclipse JDT Language Server" 23 | language = "Java" 24 | -------------------------------------------------------------------------------- /languages/java/brackets.scm: -------------------------------------------------------------------------------- 1 | ("(" @open ")" @close) 2 | ("[" @open "]" @close) 3 | ("{" @open "}" @close) 4 | -------------------------------------------------------------------------------- /languages/java/config.toml: -------------------------------------------------------------------------------- 1 | name = "Java" 2 | grammar = "java" 3 | path_suffixes = ["java"] 4 | line_comments = ["// "] 5 | autoclose_before = ";:.,=}])>` \n\t\"" 6 | brackets = [ 7 | { start = "{", end = "}", close = true, newline = true }, 8 | { start = "[", end = "]", close = true, newline = true }, 9 | { start = "(", end = ")", close = true, newline = false }, 10 | ] 11 | prettier_parser_name = "java" 12 | prettier_plugins = ["prettier-plugin-java"] 13 | -------------------------------------------------------------------------------- /languages/java/folds.scm: -------------------------------------------------------------------------------- 1 | [ 2 | (block) 3 | (class_body) 4 | (constructor_declaration) 5 | (argument_list) 6 | (annotation_argument_list) 7 | ] @fold 8 | -------------------------------------------------------------------------------- /languages/java/highlights.scm: -------------------------------------------------------------------------------- 1 | ; CREDITS @maxbrunsfeld (maxbrunsfeld@gmail.com) 2 | ; Variables 3 | (identifier) @variable 4 | 5 | ; Methods 6 | (method_declaration 7 | name: (identifier) @function) 8 | 9 | (method_invocation 10 | name: (identifier) @function) 11 | 12 | (super) @function 13 | 14 | ; Parameters 15 | (formal_parameter 16 | name: (identifier) @variable) 17 | 18 | (catch_formal_parameter 19 | name: (identifier) @variable) 20 | 21 | (spread_parameter 22 | (variable_declarator 23 | name: (identifier) @variable)) ; int... foo 24 | 25 | ; Lambda parameter 26 | (inferred_parameters 27 | (identifier) @variable) ; (x,y) -> ... 28 | 29 | (lambda_expression 30 | parameters: (identifier) @variable) ; x -> ... 31 | 32 | ; Operators 33 | [ 34 | "+" 35 | ":" 36 | "++" 37 | "-" 38 | "--" 39 | "&" 40 | "&&" 41 | "|" 42 | "||" 43 | "!" 44 | "!=" 45 | "==" 46 | "*" 47 | "/" 48 | "%" 49 | "<" 50 | "<=" 51 | ">" 52 | ">=" 53 | "=" 54 | "-=" 55 | "+=" 56 | "*=" 57 | "/=" 58 | "%=" 59 | "->" 60 | "^" 61 | "^=" 62 | "&=" 63 | "|=" 64 | "~" 65 | ">>" 66 | ">>>" 67 | "<<" 68 | "::" 69 | ] @operator 70 | 71 | ; Types 72 | (interface_declaration 73 | name: (identifier) @type) 74 | 75 | (annotation_type_declaration 76 | name: (identifier) @type) 77 | 78 | (class_declaration 79 | name: (identifier) @type) 80 | 81 | (record_declaration 82 | name: (identifier) @type) 83 | 84 | (enum_declaration 85 | name: (identifier) @enum) 86 | 87 | (enum_constant 88 | name: (identifier) @constant) 89 | 90 | (constructor_declaration 91 | name: (identifier) @constructor) 92 | 93 | (type_identifier) @type 94 | 95 | ((type_identifier) @type 96 | (#eq? @type "var")) 97 | 98 | (object_creation_expression 99 | type: (type_identifier) @constructor) 100 | 101 | ((method_invocation 102 | object: (identifier) @type) 103 | (#match? @type "^[A-Z]")) 104 | 105 | ((method_reference 106 | . 107 | (identifier) @type) 108 | (#match? @type "^[A-Z]")) 109 | 110 | ((field_access 111 | object: (identifier) @type) 112 | (#match? @type "^[A-Z]")) 113 | 114 | (scoped_identifier 115 | (identifier) @type 116 | (#match? @type "^[A-Z]")) 117 | 118 | ; Fields 119 | (field_declaration 120 | declarator: 121 | (variable_declarator 122 | name: (identifier) @property)) 123 | 124 | (field_access 125 | field: (identifier) @property) 126 | 127 | [ 128 | (boolean_type) 129 | (integral_type) 130 | (floating_point_type) 131 | (void_type) 132 | ] @type 133 | 134 | ; Variables 135 | ((identifier) @constant 136 | (#match? @constant "^[A-Z_$][A-Z\\d_$]*$")) 137 | 138 | (this) @variable 139 | 140 | ; Annotations 141 | (annotation 142 | "@" @attribute 143 | name: (identifier) @attribute) 144 | 145 | (marker_annotation 146 | "@" @attribute 147 | name: (identifier) @attribute) 148 | 149 | ; Literals 150 | (string_literal) @string 151 | 152 | (escape_sequence) @string.escape 153 | 154 | (character_literal) @string 155 | 156 | [ 157 | (hex_integer_literal) 158 | (decimal_integer_literal) 159 | (octal_integer_literal) 160 | (binary_integer_literal) 161 | (decimal_floating_point_literal) 162 | (hex_floating_point_literal) 163 | ] @number 164 | 165 | [ 166 | (true) 167 | (false) 168 | ] @boolean 169 | 170 | (null_literal) @type 171 | 172 | ; Keywords 173 | [ 174 | "assert" 175 | "class" 176 | "record" 177 | "default" 178 | "enum" 179 | "extends" 180 | "implements" 181 | "instanceof" 182 | "interface" 183 | "@interface" 184 | "permits" 185 | "to" 186 | "with" 187 | ] @keyword 188 | 189 | [ 190 | "abstract" 191 | "final" 192 | "native" 193 | "non-sealed" 194 | "open" 195 | "private" 196 | "protected" 197 | "public" 198 | "sealed" 199 | "static" 200 | "strictfp" 201 | "synchronized" 202 | "transitive" 203 | ] @keyword 204 | 205 | [ 206 | "transient" 207 | "volatile" 208 | ] @keyword 209 | 210 | [ 211 | "return" 212 | "yield" 213 | ] @keyword 214 | 215 | "new" @operator 216 | 217 | ; Conditionals 218 | [ 219 | "if" 220 | "else" 221 | "switch" 222 | "case" 223 | "when" 224 | ] @keyword 225 | 226 | (ternary_expression 227 | [ 228 | "?" 229 | ":" 230 | ] @operator) 231 | 232 | ; Loops 233 | [ 234 | "for" 235 | "while" 236 | "do" 237 | "continue" 238 | "break" 239 | ] @keyword 240 | 241 | ; Includes 242 | [ 243 | "exports" 244 | "import" 245 | "module" 246 | "opens" 247 | "package" 248 | "provides" 249 | "requires" 250 | "uses" 251 | ] @keyword 252 | 253 | ; Punctuation 254 | [ 255 | ";" 256 | "." 257 | "..." 258 | "," 259 | ] @punctuation.delimiter 260 | 261 | [ 262 | "{" 263 | "}" 264 | ] @punctuation.bracket 265 | 266 | [ 267 | "[" 268 | "]" 269 | ] @punctuation.bracket 270 | 271 | [ 272 | "(" 273 | ")" 274 | ] @punctuation.bracket 275 | 276 | (type_arguments 277 | [ 278 | "<" 279 | ">" 280 | ] @punctuation.bracket) 281 | 282 | (type_parameters 283 | [ 284 | "<" 285 | ">" 286 | ] @punctuation.bracket) 287 | 288 | (string_interpolation 289 | [ 290 | "\\{" 291 | "}" 292 | ] @string.special.symbol) 293 | 294 | ; Exceptions 295 | [ 296 | "throw" 297 | "throws" 298 | "finally" 299 | "try" 300 | "catch" 301 | ] @keyword 302 | 303 | ; Labels 304 | (labeled_statement 305 | (identifier) @label) 306 | 307 | ; Comments 308 | [ 309 | (line_comment) 310 | (block_comment) 311 | ] @comment 312 | 313 | ((block_comment) @comment.doc 314 | (#match? @comment.doc "^\\/\\*\\*")) 315 | -------------------------------------------------------------------------------- /languages/java/indents.scm: -------------------------------------------------------------------------------- 1 | (_ "{" "}" @end) @indent 2 | (_ "[" "]" @end) @indent 3 | (_ "(" ")" @end) @indent 4 | -------------------------------------------------------------------------------- /languages/java/injections.scm: -------------------------------------------------------------------------------- 1 | ([ 2 | (block_comment) 3 | (line_comment) 4 | ] @injection.content 5 | (#set! injection.language "comment")) 6 | 7 | ((block_comment) @injection.content 8 | (#lua-match? @injection.content "/[*][!<*][^a-zA-Z]") 9 | (#set! injection.language "doxygen")) 10 | 11 | ((method_invocation 12 | name: (identifier) @_method 13 | arguments: 14 | (argument_list 15 | . 16 | (string_literal 17 | . 18 | (_) @injection.content))) 19 | (#any-of? @_method "format" "printf") 20 | (#set! injection.language "printf")) 21 | 22 | ((method_invocation 23 | object: 24 | (string_literal 25 | (string_fragment) @injection.content) 26 | name: (identifier) @_method) 27 | (#eq? @_method "formatted") 28 | (#set! injection.language "printf")) 29 | -------------------------------------------------------------------------------- /languages/java/locals.scm: -------------------------------------------------------------------------------- 1 | ; SCOPES 2 | ; declarations 3 | (program) @local.scope 4 | 5 | (class_declaration 6 | body: (_) @local.scope) 7 | 8 | (record_declaration 9 | body: (_) @local.scope) 10 | 11 | (enum_declaration 12 | body: (_) @local.scope) 13 | 14 | (lambda_expression) @local.scope 15 | 16 | (enhanced_for_statement) @local.scope 17 | 18 | ; block 19 | (block) @local.scope 20 | 21 | ; if/else 22 | (if_statement) @local.scope ; if+else 23 | 24 | (if_statement 25 | consequence: (_) @local.scope) ; if body in case there are no braces 26 | 27 | (if_statement 28 | alternative: (_) @local.scope) ; else body in case there are no braces 29 | 30 | ; try/catch 31 | (try_statement) @local.scope ; covers try+catch, individual try and catch are covered by (block) 32 | 33 | (catch_clause) @local.scope ; needed because `Exception` variable 34 | 35 | ; loops 36 | (for_statement) @local.scope ; whole for_statement because loop iterator variable 37 | 38 | (for_statement 39 | ; "for" body in case there are no braces 40 | body: (_) @local.scope) 41 | 42 | (do_statement 43 | body: (_) @local.scope) 44 | 45 | (while_statement 46 | body: (_) @local.scope) 47 | 48 | ; Functions 49 | (constructor_declaration) @local.scope 50 | 51 | (method_declaration) @local.scope 52 | 53 | ; DEFINITIONS 54 | (package_declaration 55 | (identifier) @local.definition.namespace) 56 | 57 | (class_declaration 58 | name: (identifier) @local.definition.type) 59 | 60 | (record_declaration 61 | name: (identifier) @local.definition.type) 62 | 63 | (enum_declaration 64 | name: (identifier) @local.definition.enum) 65 | 66 | (method_declaration 67 | name: (identifier) @local.definition.method) 68 | 69 | (local_variable_declaration 70 | declarator: 71 | (variable_declarator 72 | name: (identifier) @local.definition.var)) 73 | 74 | (enhanced_for_statement 75 | ; for (var item : items) { 76 | name: (identifier) @local.definition.var) 77 | 78 | (formal_parameter 79 | name: (identifier) @local.definition.parameter) 80 | 81 | (catch_formal_parameter 82 | name: (identifier) @local.definition.parameter) 83 | 84 | (inferred_parameters 85 | (identifier) @local.definition.parameter) ; (x,y) -> ... 86 | 87 | (lambda_expression 88 | parameters: (identifier) @local.definition.parameter) ; x -> ... 89 | 90 | ((scoped_identifier 91 | (identifier) @local.definition.import) 92 | (#has-ancestor? @local.definition.import import_declaration)) 93 | 94 | (field_declaration 95 | declarator: 96 | (variable_declarator 97 | name: (identifier) @local.definition.field)) 98 | 99 | ; REFERENCES 100 | (identifier) @local.reference 101 | 102 | (type_identifier) @local.reference 103 | -------------------------------------------------------------------------------- /languages/java/outline.scm: -------------------------------------------------------------------------------- 1 | (class_declaration 2 | (modifiers 3 | [ 4 | "private" 5 | "public" 6 | "protected" 7 | "abstract" 8 | "sealed" 9 | "non-sealed" 10 | "final" 11 | "strictfp" 12 | "static" 13 | ]* @context) 14 | "class" @context 15 | name: (_) @name) @item 16 | 17 | (record_declaration 18 | (modifiers 19 | [ 20 | "private" 21 | "public" 22 | "protected" 23 | "abstract" 24 | "sealed" 25 | "non-sealed" 26 | "final" 27 | "strictfp" 28 | "static" 29 | ]* @context) 30 | "record" @context 31 | name: (_) @name) @item 32 | 33 | (interface_declaration 34 | (modifiers 35 | [ 36 | "private" 37 | "public" 38 | "protected" 39 | "abstract" 40 | "sealed" 41 | "non-sealed" 42 | "strictfp" 43 | "static" 44 | ]* @context) 45 | "interface" @context 46 | name: (_) @name) @item 47 | 48 | (method_declaration 49 | (modifiers 50 | [ 51 | "private" 52 | "public" 53 | "protected" 54 | "abstract" 55 | "static" 56 | "final" 57 | "native" 58 | "strictfp" 59 | "synchronized" 60 | ]* @context) 61 | name: (_) @name 62 | parameters: (formal_parameters 63 | "(" @context 64 | ")" @context)) @item 65 | 66 | (field_declaration 67 | (modifiers 68 | [ 69 | "private" 70 | "public" 71 | "protected" 72 | "static" 73 | "final" 74 | "transient" 75 | "volatile" 76 | ]* @context) 77 | declarator: (variable_declarator 78 | name: (_) @name)) @item 79 | -------------------------------------------------------------------------------- /languages/properties/config.toml: -------------------------------------------------------------------------------- 1 | name = "Properties" 2 | grammar = "properties" 3 | path_suffixes = ["properties"] 4 | line_comments = ["# "] 5 | brackets = [{ start = "[", end = "]", close = true, newline = true }] 6 | -------------------------------------------------------------------------------- /languages/properties/highlights.scm: -------------------------------------------------------------------------------- 1 | (comment) @comment 2 | 3 | (key) @property 4 | 5 | (value) @string 6 | 7 | (value (escape) @string.escape) 8 | 9 | ((index) @number 10 | (#match? @number "^[0-9]+$")) 11 | 12 | ((substitution (key) @constant) 13 | (#match? @constant "^[A-Z0-9_]+")) 14 | 15 | (substitution 16 | (key) @function 17 | "::" @punctuation.special 18 | (secret) @embedded) 19 | 20 | (property [ "=" ":" ] @operator) 21 | 22 | [ "${" "}" ] @punctuation.special 23 | 24 | (substitution ":" @punctuation.special) 25 | 26 | [ "[" "]" ] @punctuation.bracket 27 | 28 | [ "." "\\" ] @punctuation.delimiter 29 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::BTreeSet, 3 | env::current_dir, 4 | fs::{self, create_dir}, 5 | path::{Path, PathBuf}, 6 | }; 7 | 8 | use zed_extension_api::{ 9 | self as zed, CodeLabel, CodeLabelSpan, DownloadedFileType, Extension, LanguageServerId, 10 | LanguageServerInstallationStatus, Os, Worktree, current_platform, download_file, 11 | http_client::{HttpMethod, HttpRequest, fetch}, 12 | lsp::{Completion, CompletionKind}, 13 | make_file_executable, register_extension, 14 | serde_json::{self, Value}, 15 | set_language_server_installation_status, 16 | settings::LspSettings, 17 | }; 18 | 19 | const PATH_TO_STR_ERROR: &str = "failed to convert path to string"; 20 | 21 | struct Java { 22 | cached_binary_path: Option, 23 | cached_lombok_path: Option, 24 | } 25 | 26 | impl Java { 27 | fn language_server_binary_path( 28 | &mut self, 29 | language_server_id: &LanguageServerId, 30 | worktree: &Worktree, 31 | ) -> zed::Result { 32 | // Use cached path if exists 33 | 34 | if let Some(path) = &self.cached_binary_path { 35 | if fs::metadata(path).is_ok_and(|stat| stat.is_file()) { 36 | return Ok(path.clone()); 37 | } 38 | } 39 | 40 | // Use $PATH if binary is in it 41 | 42 | let (platform, _) = current_platform(); 43 | let binary_name = match platform { 44 | Os::Windows => "jdtls.bat", 45 | _ => "jdtls", 46 | }; 47 | 48 | if let Some(path_binary) = worktree.which(binary_name) { 49 | return Ok(PathBuf::from(path_binary)); 50 | } 51 | 52 | // Check for latest version 53 | 54 | set_language_server_installation_status( 55 | language_server_id, 56 | &LanguageServerInstallationStatus::CheckingForUpdate, 57 | ); 58 | 59 | // Yeah, this part's all pretty terrible... 60 | // Note to self: make it good eventually 61 | let downloads_html = String::from_utf8( 62 | fetch( 63 | &HttpRequest::builder() 64 | .method(HttpMethod::Get) 65 | .url("https://download.eclipse.org/jdtls/milestones/") 66 | .build()?, 67 | ) 68 | .map_err(|err| format!("failed to get available versions: {err}"))? 69 | .body, 70 | ) 71 | .map_err(|err| format!("could not get string from downloads page response body: {err}"))?; 72 | let mut versions = BTreeSet::new(); 73 | let mut number_buffer = String::new(); 74 | let mut version_buffer: (Option, Option, Option) = (None, None, None); 75 | 76 | for char in downloads_html.chars() { 77 | if char.is_numeric() { 78 | number_buffer.push(char); 79 | } else if char == '.' { 80 | if version_buffer.0.is_none() && !number_buffer.is_empty() { 81 | version_buffer.0 = Some( 82 | number_buffer 83 | .parse() 84 | .map_err(|err| format!("could not parse number buffer: {err}"))?, 85 | ); 86 | } else if version_buffer.1.is_none() && !number_buffer.is_empty() { 87 | version_buffer.1 = Some( 88 | number_buffer 89 | .parse() 90 | .map_err(|err| format!("could not parse number buffer: {err}"))?, 91 | ); 92 | } else { 93 | version_buffer = (None, None, None); 94 | } 95 | 96 | number_buffer.clear(); 97 | } else { 98 | if version_buffer.0.is_some() 99 | && version_buffer.1.is_some() 100 | && version_buffer.2.is_none() 101 | { 102 | versions.insert(( 103 | version_buffer.0.ok_or("no major version number")?, 104 | version_buffer.1.ok_or("no minor version number")?, 105 | number_buffer 106 | .parse::() 107 | .map_err(|err| format!("could not parse number buffer: {err}"))?, 108 | )); 109 | } 110 | 111 | number_buffer.clear(); 112 | version_buffer = (None, None, None); 113 | } 114 | } 115 | 116 | let (major, minor, patch) = versions.last().ok_or("no available versions")?; 117 | let latest_version = format!("{major}.{minor}.{patch}"); 118 | let latest_version_build = String::from_utf8( 119 | fetch( 120 | &HttpRequest::builder() 121 | .method(HttpMethod::Get) 122 | .url(format!( 123 | "https://download.eclipse.org/jdtls/milestones/{latest_version}/latest.txt" 124 | )) 125 | .build()?, 126 | ) 127 | .map_err(|err| format!("failed to get latest version's build: {err}"))? 128 | .body, 129 | ) 130 | .map_err(|err| { 131 | format!("attempt to get latest version's build resulted in a malformed response: {err}") 132 | })?; 133 | let latest_version_build = latest_version_build.trim_end(); 134 | let prefix = PathBuf::from("jdtls"); 135 | // Exclude ".tar.gz" 136 | let build_directory = &latest_version_build[..latest_version_build.len() - 7]; 137 | let build_path = prefix.join(build_directory); 138 | let binary_path = build_path.join("bin").join(binary_name); 139 | 140 | // If latest version isn't installed, 141 | if !fs::metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { 142 | // then download it... 143 | 144 | set_language_server_installation_status( 145 | language_server_id, 146 | &LanguageServerInstallationStatus::Downloading, 147 | ); 148 | download_file( 149 | &format!( 150 | "https://www.eclipse.org/downloads/download.php?file=/jdtls/milestones/{latest_version}/{latest_version_build}", 151 | ), 152 | build_path.to_str().ok_or(PATH_TO_STR_ERROR)?, 153 | DownloadedFileType::GzipTar, 154 | )?; 155 | make_file_executable(binary_path.to_str().ok_or(PATH_TO_STR_ERROR)?)?; 156 | 157 | // ...and delete other versions 158 | 159 | // This step is expected to fail sometimes, and since we don't know 160 | // how to fix it yet, we just carry on so the user doesn't have to 161 | // restart the language server. 162 | match fs::read_dir(prefix) { 163 | Ok(entries) => { 164 | for entry in entries { 165 | match entry { 166 | Ok(entry) => { 167 | if entry.file_name().to_str() != Some(build_directory) { 168 | if let Err(err) = fs::remove_dir_all(entry.path()) { 169 | println!("failed to remove directory entry: {err}"); 170 | } 171 | } 172 | } 173 | Err(err) => println!("failed to load directory entry: {err}"), 174 | } 175 | } 176 | } 177 | Err(err) => println!("failed to list prefix directory: {err}"), 178 | } 179 | } 180 | 181 | // else use it 182 | 183 | self.cached_binary_path = Some(binary_path.clone()); 184 | 185 | Ok(binary_path) 186 | } 187 | 188 | fn lombok_jar_path(&mut self, language_server_id: &LanguageServerId) -> zed::Result { 189 | // Use cached path if exists 190 | 191 | if let Some(path) = &self.cached_lombok_path { 192 | if fs::metadata(path).is_ok_and(|stat| stat.is_file()) { 193 | return Ok(path.clone()); 194 | } 195 | } 196 | 197 | // Check for latest version 198 | 199 | set_language_server_installation_status( 200 | language_server_id, 201 | &LanguageServerInstallationStatus::CheckingForUpdate, 202 | ); 203 | 204 | let tags_response_body = serde_json::from_slice::( 205 | &fetch( 206 | &HttpRequest::builder() 207 | .method(HttpMethod::Get) 208 | .url("https://api.github.com/repos/projectlombok/lombok/tags") 209 | .build()?, 210 | ) 211 | .map_err(|err| format!("failed to fetch GitHub tags: {err}"))? 212 | .body, 213 | ) 214 | .map_err(|err| format!("failed to deserialize GitHub tags response: {err}"))?; 215 | let latest_version = &tags_response_body 216 | .as_array() 217 | .and_then(|tag| { 218 | tag.first().and_then(|latest_tag| { 219 | latest_tag 220 | .get("name") 221 | .and_then(|tag_name| tag_name.as_str()) 222 | }) 223 | }) 224 | // Exclude 'v' at beginning 225 | .ok_or("malformed GitHub tags response")?[1..]; 226 | let prefix = "lombok"; 227 | let jar_name = format!("lombok-{latest_version}.jar"); 228 | let jar_path = Path::new(prefix).join(&jar_name); 229 | 230 | // If latest version isn't installed, 231 | if !fs::metadata(&jar_path).is_ok_and(|stat| stat.is_file()) { 232 | // then download it... 233 | 234 | set_language_server_installation_status( 235 | language_server_id, 236 | &LanguageServerInstallationStatus::Downloading, 237 | ); 238 | create_dir(prefix).map_err(|err| err.to_string())?; 239 | download_file( 240 | &format!("https://projectlombok.org/downloads/{jar_name}"), 241 | jar_path.to_str().ok_or(PATH_TO_STR_ERROR)?, 242 | DownloadedFileType::Uncompressed, 243 | )?; 244 | 245 | // ...and delete other versions 246 | 247 | // This step is expected to fail sometimes, and since we don't know 248 | // how to fix it yet, we just carry on so the user doesn't have to 249 | // restart the language server. 250 | match fs::read_dir(prefix) { 251 | Ok(entries) => { 252 | for entry in entries { 253 | match entry { 254 | Ok(entry) => { 255 | if entry.file_name().to_str() != Some(&jar_name) { 256 | if let Err(err) = fs::remove_dir_all(entry.path()) { 257 | println!("failed to remove directory entry: {err}"); 258 | } 259 | } 260 | } 261 | Err(err) => println!("failed to load directory entry: {err}"), 262 | } 263 | } 264 | } 265 | Err(err) => println!("failed to list prefix directory: {err}"), 266 | } 267 | } 268 | 269 | // else use it 270 | 271 | self.cached_lombok_path = Some(jar_path.clone()); 272 | 273 | Ok(jar_path) 274 | } 275 | } 276 | 277 | impl Extension for Java { 278 | fn new() -> Self 279 | where 280 | Self: Sized, 281 | { 282 | Self { 283 | cached_binary_path: None, 284 | cached_lombok_path: None, 285 | } 286 | } 287 | 288 | fn language_server_command( 289 | &mut self, 290 | language_server_id: &LanguageServerId, 291 | worktree: &Worktree, 292 | ) -> zed::Result { 293 | let configuration = 294 | self.language_server_workspace_configuration(language_server_id, worktree)?; 295 | let java_home = configuration.as_ref().and_then(|configuration| { 296 | configuration 297 | .pointer("/java/home") 298 | .and_then(|java_home_value| { 299 | java_home_value 300 | .as_str() 301 | .map(|java_home_str| java_home_str.to_string()) 302 | }) 303 | }); 304 | let mut env = Vec::new(); 305 | 306 | if let Some(java_home) = java_home { 307 | env.push(("JAVA_HOME".to_string(), java_home)); 308 | } 309 | 310 | let mut args = Vec::new(); 311 | // Add lombok as javaagent if settings.java.jdt.ls.lombokSupport.enabled is true 312 | let lombok_enabled = configuration 313 | .and_then(|configuration| { 314 | configuration 315 | .pointer("/java/jdt/ls/lombokSupport/enabled") 316 | .and_then(|enabled| enabled.as_bool()) 317 | }) 318 | .unwrap_or(false); 319 | 320 | if lombok_enabled { 321 | let mut current_dir = 322 | current_dir().map_err(|err| format!("could not get current dir: {err}"))?; 323 | 324 | if current_platform().0 == Os::Windows { 325 | current_dir = current_dir 326 | .strip_prefix("/") 327 | .map_err(|err| err.to_string())? 328 | .to_path_buf(); 329 | } 330 | 331 | let lombok_jar_path = self.lombok_jar_path(language_server_id)?; 332 | let canonical_lombok_jar_path = current_dir 333 | .join(lombok_jar_path) 334 | .to_str() 335 | .ok_or(PATH_TO_STR_ERROR)? 336 | .to_string(); 337 | 338 | args.push(format!("--jvm-arg=-javaagent:{canonical_lombok_jar_path}")); 339 | } 340 | 341 | Ok(zed::Command { 342 | command: self 343 | .language_server_binary_path(language_server_id, worktree)? 344 | .to_str() 345 | .ok_or(PATH_TO_STR_ERROR)? 346 | .to_string(), 347 | args, 348 | env, 349 | }) 350 | } 351 | 352 | fn language_server_initialization_options( 353 | &mut self, 354 | language_server_id: &LanguageServerId, 355 | worktree: &Worktree, 356 | ) -> zed::Result> { 357 | LspSettings::for_worktree(language_server_id.as_ref(), worktree) 358 | .map(|lsp_settings| lsp_settings.initialization_options) 359 | } 360 | 361 | fn language_server_workspace_configuration( 362 | &mut self, 363 | language_server_id: &LanguageServerId, 364 | worktree: &Worktree, 365 | ) -> zed::Result> { 366 | // FIXME(Valentine Briese): I don't really like that we have a variable 367 | // here, there're probably some `Result` and/or 368 | // `Option` methods that would eliminate the 369 | // need for this, but at least this is easy to 370 | // read. 371 | 372 | let mut settings = LspSettings::for_worktree(language_server_id.as_ref(), worktree) 373 | .map(|lsp_settings| lsp_settings.settings); 374 | 375 | if !matches!(settings, Ok(Some(_))) { 376 | settings = self 377 | .language_server_initialization_options(language_server_id, worktree) 378 | .map(|initialization_options| { 379 | initialization_options.and_then(|initialization_options| { 380 | initialization_options.get("settings").cloned() 381 | }) 382 | }) 383 | } 384 | 385 | settings 386 | } 387 | 388 | fn label_for_completion( 389 | &self, 390 | _language_server_id: &LanguageServerId, 391 | completion: Completion, 392 | ) -> Option { 393 | // uncomment when debugging completions 394 | // println!("Java completion: {completion:#?}"); 395 | 396 | completion.kind.and_then(|kind| match kind { 397 | CompletionKind::Field | CompletionKind::Constant => { 398 | let modifiers = match kind { 399 | CompletionKind::Field => "", 400 | CompletionKind::Constant => "static final ", 401 | _ => return None, 402 | }; 403 | let property_type = completion.detail.as_ref().and_then(|detail| { 404 | detail 405 | .split_once(" : ") 406 | .map(|(_, property_type)| format!("{property_type} ")) 407 | })?; 408 | let semicolon = ";"; 409 | let code = format!("{modifiers}{property_type}{}{semicolon}", completion.label); 410 | 411 | Some(CodeLabel { 412 | spans: vec![ 413 | CodeLabelSpan::code_range( 414 | modifiers.len() + property_type.len()..code.len() - semicolon.len(), 415 | ), 416 | CodeLabelSpan::literal(" : ", None), 417 | CodeLabelSpan::code_range( 418 | modifiers.len()..modifiers.len() + property_type.len(), 419 | ), 420 | ], 421 | code, 422 | filter_range: (0..completion.label.len()).into(), 423 | }) 424 | } 425 | CompletionKind::Method => { 426 | let detail = completion.detail?; 427 | let (left, return_type) = detail 428 | .split_once(" : ") 429 | .map(|(left, return_type)| (left, format!("{return_type} "))) 430 | .unwrap_or((&detail, "void".to_string())); 431 | let parameters = left 432 | .find('(') 433 | .map(|parameters_start| &left[parameters_start..]); 434 | let name_and_parameters = 435 | format!("{}{}", completion.label, parameters.unwrap_or("()")); 436 | let braces = " {}"; 437 | let code = format!("{return_type}{name_and_parameters}{braces}"); 438 | let mut spans = vec![CodeLabelSpan::code_range( 439 | return_type.len()..code.len() - braces.len(), 440 | )]; 441 | 442 | if parameters.is_some() { 443 | spans.push(CodeLabelSpan::literal(" : ", None)); 444 | spans.push(CodeLabelSpan::code_range(0..return_type.len())); 445 | } else { 446 | spans.push(CodeLabelSpan::literal(" - ", None)); 447 | spans.push(CodeLabelSpan::literal(detail, None)); 448 | } 449 | 450 | Some(CodeLabel { 451 | spans, 452 | code, 453 | filter_range: (0..completion.label.len()).into(), 454 | }) 455 | } 456 | CompletionKind::Class | CompletionKind::Interface | CompletionKind::Enum => { 457 | let keyword = match kind { 458 | CompletionKind::Class => "class ", 459 | CompletionKind::Interface => "interface ", 460 | CompletionKind::Enum => "enum ", 461 | _ => return None, 462 | }; 463 | let braces = " {}"; 464 | let code = format!("{keyword}{}{braces}", completion.label); 465 | let namespace = completion 466 | .detail 467 | .map(|detail| detail[..detail.len() - completion.label.len() - 1].to_string()); 468 | let mut spans = vec![CodeLabelSpan::code_range( 469 | keyword.len()..code.len() - braces.len(), 470 | )]; 471 | 472 | if let Some(namespace) = namespace { 473 | spans.push(CodeLabelSpan::literal(format!(" ({namespace})"), None)); 474 | } 475 | 476 | Some(CodeLabel { 477 | spans, 478 | code, 479 | filter_range: (0..completion.label.len()).into(), 480 | }) 481 | } 482 | CompletionKind::Snippet => Some(CodeLabel { 483 | code: String::new(), 484 | spans: vec![CodeLabelSpan::literal( 485 | format!("{} - {}", completion.label, completion.detail?), 486 | None, 487 | )], 488 | filter_range: (0..completion.label.len()).into(), 489 | }), 490 | CompletionKind::Keyword | CompletionKind::Variable => Some(CodeLabel { 491 | spans: vec![CodeLabelSpan::code_range(0..completion.label.len())], 492 | filter_range: (0..completion.label.len()).into(), 493 | code: completion.label, 494 | }), 495 | CompletionKind::Constructor => { 496 | let detail = completion.detail?; 497 | let parameters = &detail[detail.find('(')?..]; 498 | let braces = " {}"; 499 | let code = format!("{}{parameters}{braces}", completion.label); 500 | 501 | Some(CodeLabel { 502 | spans: vec![CodeLabelSpan::code_range(0..code.len() - braces.len())], 503 | code, 504 | filter_range: (0..completion.label.len()).into(), 505 | }) 506 | } 507 | _ => None, 508 | }) 509 | } 510 | } 511 | 512 | register_extension!(Java); 513 | --------------------------------------------------------------------------------