├── .gitignore ├── languages └── luau │ ├── overrides.scm │ ├── runnables.scm │ ├── brackets.scm │ ├── textobjects.scm │ ├── indents.scm │ ├── config.toml │ ├── outline.scm │ └── highlights.scm ├── Cargo.toml ├── extension.toml ├── LICENSE ├── src ├── roblox.rs └── luau.rs ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | grammars 3 | extension.wasm 4 | Cargo.lock 5 | -------------------------------------------------------------------------------- /languages/luau/overrides.scm: -------------------------------------------------------------------------------- 1 | (comment) @comment.inclusive 2 | [ 3 | (string) 4 | (string_content) 5 | (interpolated_string 6 | [ 7 | "`" 8 | ]) 9 | ] @string 10 | -------------------------------------------------------------------------------- /languages/luau/runnables.scm: -------------------------------------------------------------------------------- 1 | ; Jest Lua test runnable. 2 | ( 3 | (function_call 4 | name: (identifier) @_name 5 | (#any-of? @_name "it" "test" "describe") 6 | arguments: (arguments 7 | (string 8 | content: (_) @run @script))) @_luau-jest-test 9 | (#set! tag luau-jest-test)) 10 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zed-luau" 3 | version = "0.3.3" 4 | edition = "2024" 5 | publish = false 6 | license = "MIT" 7 | 8 | [lib] 9 | path = "src/luau.rs" 10 | crate-type = ["cdylib"] 11 | 12 | [dependencies] 13 | zed_extension_api = "0.7.0" 14 | serde = { version = "1.0", default-features = false, features = ["derive"]} 15 | serde_path_to_error = "0.1" 16 | -------------------------------------------------------------------------------- /languages/luau/brackets.scm: -------------------------------------------------------------------------------- 1 | ("[" @open "]" @close) 2 | ("{" @open "}" @close) 3 | ("(" @open ")" @close) 4 | ("'" @open "'" @close) 5 | ("`" @open "`" @close) 6 | ("<" @open ">" @close) 7 | ("\"" @open "\"" @close) 8 | ("[[" @open "]]" @close) 9 | ("then" @open "end" @close) 10 | ("do" @open "end" @close) 11 | ("function" @open "end" @close) 12 | ("repeat" @open "until" @close) 13 | -------------------------------------------------------------------------------- /extension.toml: -------------------------------------------------------------------------------- 1 | id = "luau" 2 | name = "Luau" 3 | version = "0.3.3" 4 | schema_version = 1 5 | authors = ["teapo <4teapo@gmail.com>"] 6 | description = "Luau support." 7 | repository = "https://github.com/4teapo/zed-luau" 8 | 9 | [language_servers.luau-lsp] 10 | name = "Luau LSP" 11 | language = "Luau" 12 | 13 | [grammars.luau] 14 | repository = "https://github.com/4teapo/tree-sitter-luau" 15 | commit = "01eb0b61f8f7efdc1ad4f0ed1bd3cd97be7f60fd" 16 | -------------------------------------------------------------------------------- /languages/luau/textobjects.scm: -------------------------------------------------------------------------------- 1 | (function_definition 2 | body: (_)* @function.inside) @function.around 3 | 4 | (function_declaration 5 | body: (_)* @function.inside) @function.around 6 | 7 | (local_function_declaration 8 | body: (_)* @function.inside) @function.around 9 | 10 | (type_alias_declaration 11 | type: (_)* @class.inside) @class.around 12 | 13 | (type_function_declaration 14 | body: (_)* @function.inside) @function.around 15 | 16 | (comment)+ @comment.around 17 | -------------------------------------------------------------------------------- /languages/luau/indents.scm: -------------------------------------------------------------------------------- 1 | (if_statement "end" @end) @indent 2 | (do_statement "end" @end) @indent 3 | (while_statement "end" @end) @indent 4 | (for_statement "end" @end) @indent 5 | (repeat_statement "until" @end) @indent 6 | (function_declaration "end" @end) @indent 7 | (local_function_declaration "end" @end) @indent 8 | (type_function_declaration "end" @end) @indent 9 | (function_definition "end" @end) @indent 10 | 11 | (_ "[" "]" @end) @indent 12 | (_ "{" "}" @end) @indent 13 | (_ "(" ")" @end) @indent 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 teapo 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. 22 | -------------------------------------------------------------------------------- /languages/luau/config.toml: -------------------------------------------------------------------------------- 1 | name = "Luau" 2 | grammar = "luau" 3 | path_suffixes = ["luau"] 4 | line_comments = ["-- ", "--- "] 5 | block_comment = ["--[", "]"] 6 | autoclose_before = ";:.,=}])>" 7 | first_line_pattern = "^#!.*\b(lune|luau|zune)\b" 8 | brackets = [ 9 | { start = "{", end = "}", close = true, newline = true }, 10 | { start = "[", end = "]", close = true, newline = true }, 11 | { start = "(", end = ")", close = true, newline = true }, 12 | { start = "\"", end = "\"", close = true, newline = false, not_in = [ "comment", "string" ] }, 13 | { start = "'", end = "'", close = true, newline = false, not_in = [ "comment", "string" ] }, 14 | { start = "`", end = "`", close = true, newline = false, not_in = [ "comment", "string" ] }, 15 | { start = "<", end = ">", close = false, newline = false, not_in = [ "comment", "string" ] }, 16 | { start = "then", end = "end", close = false, newline = true, not_in = [ "comment", "string" ] }, 17 | { start = "do", end = "end", close = false, newline = true, not_in = [ "comment", "string" ] }, 18 | { start = "function", end = "end", close = false, newline = true, not_in = [ "comment", "string" ] }, 19 | { start = "repeat", end = "until", close = false, newline = true, not_in = [ "comment", "string" ] }, 20 | ] 21 | tab_size = 4 22 | -------------------------------------------------------------------------------- /src/roblox.rs: -------------------------------------------------------------------------------- 1 | use zed_extension_api::{self as zed, Result}; 2 | 3 | const API_DOCS_URL: &str = "https://raw.githubusercontent.com/MaximumADHD/Roblox-Client-Tracker/roblox/api-docs/en-us.json"; 4 | pub const SECURITY_LEVEL_NONE: &str = "None"; 5 | pub const SECURITY_LEVEL_LOCAL_USER: &str = "LocalUserSecurity"; 6 | pub const SECURITY_LEVEL_PLUGIN: &str = "PluginSecurity"; 7 | pub const SECURITY_LEVEL_ROBLOX_SCRIPT: &str = "RobloxScriptSecurity"; 8 | pub const API_DOCS_FILE_NAME: &str = "api-docs.json"; 9 | 10 | pub fn get_definitions_url_for_level(level: &str) -> String { 11 | format!( 12 | "https://raw.githubusercontent.com/JohnnyMorganz/luau-lsp/main/scripts/globalTypes.{}.d.luau", 13 | level 14 | ) 15 | } 16 | 17 | pub fn get_definitions_file_for_level(level: &str) -> String { 18 | format!("globalTypes.{}.d.luau", level) 19 | } 20 | 21 | pub fn download_api_docs() -> Result<()> { 22 | zed::download_file( 23 | API_DOCS_URL, 24 | API_DOCS_FILE_NAME, 25 | zed::DownloadedFileType::Uncompressed, 26 | )?; 27 | Ok(()) 28 | } 29 | 30 | pub fn download_definitions(security_level: &str) -> Result<()> { 31 | let url = get_definitions_url_for_level(security_level); 32 | zed::download_file( 33 | &url, 34 | &get_definitions_file_for_level(security_level), 35 | zed::DownloadedFileType::Uncompressed, 36 | )?; 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /languages/luau/outline.scm: -------------------------------------------------------------------------------- 1 | (comment) @annotation 2 | 3 | (local_variable_declaration 4 | "local" @context 5 | (binding_list 6 | (binding 7 | name: (identifier) @name 8 | (#match? @name "^[A-Z][A-Z][A-Z_0-9]*$")) @item)) 9 | 10 | (type_alias_declaration 11 | "export"? @context 12 | "type" @context 13 | name: (type_identifier) @name) @item 14 | 15 | (type_alias_declaration 16 | type: (table_type 17 | (table_property_list 18 | [ 19 | (table_property 20 | attribute: (table_property_attribute)? @context 21 | left: (field_identifier) @name)? 22 | (table_indexer 23 | attribute: (table_property_attribute)? @context 24 | "[" @context 25 | (_) @name 26 | "]" @context)? 27 | ] @item))) 28 | 29 | (type_function_declaration 30 | "export"? @context 31 | "type" @context 32 | "function" @context 33 | name: (type_identifier) @name 34 | (parameters 35 | "(" @context 36 | ")" @context)) @item 37 | 38 | (function_declaration 39 | "function" @context 40 | name: (_) @name 41 | (parameters 42 | "(" @context 43 | ")" @context)) @item 44 | 45 | (local_function_declaration 46 | "local" @context 47 | "function" @context 48 | name: (_) @name 49 | (parameters 50 | "(" @context 51 | ")" @context)) @item 52 | 53 | (declare_global_declaration 54 | "declare" @context 55 | name: (identifier) @name) @item 56 | 57 | (declare_global_declaration 58 | type: (table_type 59 | (table_property_list 60 | [ 61 | (table_property 62 | attribute: (table_property_attribute)? @context 63 | left: (field_identifier) @name)? 64 | (table_indexer 65 | attribute: (table_property_attribute)? @context 66 | "[" @context 67 | (_) @name 68 | "]" @context)? 69 | ] @item))) 70 | 71 | (declare_global_function_declaration 72 | "declare" @context 73 | "function" @context 74 | name: (identifier) @name 75 | (parameters 76 | "(" @context 77 | ")" @context)) @item 78 | 79 | (declare_class_declaration 80 | "declare" @context 81 | "class" @context 82 | name: (identifier) @name 83 | "extends"? @context 84 | superclass: (identifier)? @name) @item 85 | 86 | (declare_extern_type_declaration 87 | "declare" @context 88 | "extern" @context 89 | "type" @context 90 | name: (identifier) @name 91 | "extends"? @context 92 | supertype: (identifier)? @name) @item 93 | 94 | (extern_type_property 95 | left: (field_identifier) @name) @item 96 | 97 | (extern_type_indexer 98 | "[" @context 99 | (_) @name 100 | "]" @context) @item 101 | 102 | (class_function 103 | "function" @context 104 | name: (identifier) @name 105 | (parameters 106 | "(" @context 107 | ")" @context)) @item 108 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.3.3] - 2025-10-16 11 | 12 | ### Fixed 13 | 14 | - Fixed error when having multiple windows on Windows 15 | 16 | ## [0.3.2] - 2025-10-1 17 | 18 | ### Changed 19 | 20 | - Bumped extension API to latest version. 21 | - Removed now-unnecessary Windows `current_dir` workaround. 22 | 23 | ## [0.3.1] - 2025-8-2 24 | 25 | ### Added 26 | 27 | - Added support for `declare extern type with` syntax 28 | 29 | ### Fixed 30 | 31 | - Fixed binary auto-install failing on X86-64 linux. 32 | - Fixed line comment being automatically inserted after pressing enter at the end of a line that 33 | starts a block comment. 34 | 35 | ## [0.3.0] - 2025-7-4 36 | 37 | ### Added 38 | 39 | - Added support for the companion plugin. 40 | - Added support for putting extension settings directly under `lsp.luau-lsp.settings`. 41 | * This is now recommended but *if zed-luau finds `ext` in settings (project settings or regular 42 | settings, project settings being preferred), it will prefer that and ignore other settings.* 43 | - Added support for text objects ([docs](https://zed.dev/docs/vim#treesitter)). 44 | - Added settings for more fine-grained control over Roblox-related behavior. 45 | * roblox.download_api_documentation 46 | * roblox.download_definitions 47 | 48 | ### Fixed 49 | 50 | - Fixed default security level not being plugin. 51 | - Fixed being unable to depend on automatically added Roblox types in additional definition files. 52 | - Fixed doc comments not being inserted with `extend_comment_on_newline`. 53 | 54 | ## [0.2.2] - 2024-12-22 55 | 56 | ### Added 57 | 58 | - Added support for declaration syntax. 59 | - Added setting ext.fflags.enable_new_solver. 60 | 61 | ### Changed 62 | 63 | - Updated to the latest grammar commit. 64 | - Made syntax highlighting captures more specific. 65 | - Constrained special highlighting for `string` to only when it's the table in a dot index 66 | expression. 67 | 68 | ## [0.2.1] - 2024-11-24 69 | 70 | ### Added 71 | 72 | - Added outline support with annotations and special handling for table types. 73 | 74 | ### Changed 75 | 76 | - Changed the grammar for Luau to [4teapo/tree-sitter-luau](https://github.com/4teapo/tree-sitter-luau) 77 | for improved syntax highlighting. 78 | - Changed default script security level to plugin security. 79 | 80 | ### Removed 81 | 82 | - Removed automatic closing for `<`/`>`. 83 | 84 | ### Fixed 85 | 86 | - Fixed bracket pairs inside of comments automatically closing. 87 | 88 | ## [0.2.0] - 2024-10-26 89 | 90 | ### Added 91 | 92 | - Added `ext.binary` settings, including `ext.binary.path` and `ext.binary.ignore_system_version`. 93 | - The following pairs of keywords now get highlighted as brackets: 94 | - `then`/`end` 95 | - `do`/`end` 96 | - `function`/`end` 97 | - `repeat`/`until` 98 | - `"`/`"` 99 | - \`/\` 100 | - `<`/`>` 101 | - `[[`/`]]` 102 | - Added support for FInt, DFFlag, and DFInt FFlags. 103 | - Added automatic indentation support. 104 | - Added support for automatically closing ', ` and <. They're currently inserted automatically in 105 | comments as well due to a bug in Zed. 106 | - Added runnable for Jest Lua tests (`it("", ...)`, `describe("", ...)`, and `test("", ...)`) with 107 | the tag `luau-jest-test`. 108 | 109 | ### Changed 110 | 111 | - Changed error messages for configuration faults. 112 | - Changed the default value of `ext.fflags.enable_by_default` from `true` to `false`. 113 | - Changed the default value of `ext.fflags.sync` from `false` to `true`. 114 | 115 | ### Removed 116 | 117 | - Removed the `ext.prefer_worktree_binary` setting in favor of `ext.binary.ignore_system_version`. 118 | -------------------------------------------------------------------------------- /languages/luau/highlights.scm: -------------------------------------------------------------------------------- 1 | ; Keywords 2 | 3 | [ 4 | "local" 5 | "while" 6 | "repeat" 7 | "until" 8 | "for" 9 | "in" 10 | "if" 11 | "elseif" 12 | "else" 13 | "then" 14 | "do" 15 | "function" 16 | "end" 17 | "return" 18 | (continue_statement) 19 | (break_statement) 20 | ] @keyword 21 | 22 | (type_alias_declaration 23 | [ 24 | "export" 25 | "type" 26 | ] @keyword) 27 | 28 | (type_function_declaration 29 | [ 30 | "export" 31 | "type" 32 | ] @keyword) 33 | 34 | (declare_global_declaration 35 | "declare" @keyword) 36 | 37 | (declare_global_function_declaration 38 | "declare" @keyword) 39 | 40 | (declare_class_declaration 41 | [ 42 | "declare" 43 | "class" 44 | "extends" 45 | ] @keyword) 46 | 47 | (declare_extern_type_declaration 48 | [ 49 | "declare" 50 | "extern" 51 | "type" 52 | "extends" 53 | "with" 54 | ] @keyword) 55 | 56 | ; Punctuations 57 | 58 | [ 59 | "(" 60 | ")" 61 | "[" 62 | "]" 63 | "{" 64 | "}" 65 | "<" 66 | ">" 67 | ] @punctuation.bracket 68 | 69 | [ 70 | ";" 71 | ":" 72 | "," 73 | "." 74 | "->" 75 | ] @punctuation.delimiter 76 | 77 | ; Operators 78 | 79 | (binary_expression 80 | [ 81 | "<" 82 | ">" 83 | ] @operator.comparison) 84 | 85 | [ 86 | "==" 87 | "~=" 88 | "<=" 89 | ">=" 90 | ] @operator.comparison 91 | 92 | [ 93 | "not" 94 | "and" 95 | "or" 96 | ] @operator.logical 97 | 98 | [ 99 | "=" 100 | "+=" 101 | "-=" 102 | "*=" 103 | "/=" 104 | "//=" 105 | "%=" 106 | "^=" 107 | "..=" 108 | ] @operator.assignment 109 | 110 | [ 111 | "+" 112 | "-" 113 | "*" 114 | "/" 115 | "//" 116 | "%" 117 | "^" 118 | ] @operator.arithmetic 119 | 120 | [ 121 | "#" 122 | "&" 123 | "|" 124 | "::" 125 | ".." 126 | "?" 127 | ] @operator 128 | 129 | ; Variables 130 | 131 | (identifier) @variable 132 | 133 | (string_interpolation 134 | [ 135 | "{" 136 | "}" 137 | ] @punctuation.special) @embedded 138 | 139 | (type_binding 140 | (identifier) @variable.parameter) 141 | 142 | ((identifier) @variable.special 143 | (#any-of? @variable.special "math" "table" "coroutine" "bit32" "utf8" "os" "debug" "buffer" 144 | "vector")) 145 | 146 | ((identifier) @variable.special 147 | (#match? @variable.special "^_[A-Z]*$")) 148 | 149 | ; Tables 150 | 151 | (table_constructor 152 | [ 153 | "{" 154 | "}" 155 | ] @constructor) 156 | 157 | (field_identifier) @property 158 | 159 | ; Constants 160 | 161 | (nil) @constant.builtin 162 | 163 | ((identifier) @constant.builtin 164 | (#eq? @constant.builtin "_VERSION")) 165 | 166 | ( 167 | [ 168 | (identifier) 169 | (field_identifier) 170 | ] @constant 171 | (#match? @constant "^[A-Z][A-Z][A-Z_0-9]*$")) 172 | 173 | ; Literals 174 | 175 | (number) @number 176 | 177 | [ 178 | (true) 179 | (false) 180 | ] @boolean 181 | 182 | (string) @string 183 | (escape_sequence) @string.escape 184 | 185 | (string_interpolation 186 | [ 187 | "{" 188 | "}" 189 | ] @punctuation.special) 190 | 191 | (interpolated_string 192 | [ 193 | "`" 194 | ] @string) 195 | 196 | (string_content) @string 197 | 198 | ; Types 199 | 200 | (table_property_attribute) @attribute 201 | 202 | (typeof_type 203 | "typeof" @function.builtin) 204 | 205 | (type_identifier) @type 206 | 207 | (type_reference 208 | prefix: (identifier) @variable.namespace) 209 | 210 | (type_reference 211 | prefix: (identifier) @constant.namespace 212 | (#match? @constant.namespace "^[A-Z][A-Z][A-Z_0-9]*$")) 213 | 214 | ; Functions 215 | 216 | (function_declaration 217 | name: [ 218 | (identifier) @function 219 | (dot_index_expression 220 | field: (field_identifier) @function) 221 | ]) 222 | 223 | (method_index_expression 224 | method: (field_identifier) @function) 225 | 226 | (local_function_declaration 227 | name: (identifier) @function) 228 | 229 | (declare_global_function_declaration 230 | name: (identifier) @function) 231 | 232 | (class_function 233 | name: (identifier) @function) 234 | 235 | (parameters 236 | [ 237 | (binding 238 | name: (identifier) @variable.parameter) 239 | (variadic_parameter "..." @variable.parameter)]) 240 | 241 | ((identifier) @variable.special 242 | (#eq? @variable.special "self")) 243 | 244 | (function_call 245 | name: [ 246 | (identifier) @function 247 | (dot_index_expression 248 | field: (field_identifier) @function) 249 | ]) 250 | 251 | (parameter_attribute 252 | name: (identifier) @attribute) 253 | 254 | (attribute 255 | [ 256 | "@" @attribute 257 | name: (identifier) @attribute 258 | ]) 259 | 260 | ; require(""), (require)("") 261 | (function_call 262 | name: [ 263 | (identifier) @function.builtin 264 | (parenthesized_expression 265 | (identifier) @function.builtin) 266 | ] 267 | (#any-of? @function.builtin 268 | "require" "assert" "error" "gcinfo" "getfenv" "getmetatable" "next" 269 | "newproxy" "print" "rawequal" "rawget" "select" "setfenv" "setmetatable" 270 | "tonumber" "tostring" "type" "typeof" "ipairs" "pairs" "pcall" "xpcall" 271 | "unpack")) 272 | 273 | ; tbl.__index, tbl:__index 274 | (function_call 275 | name: [ 276 | (dot_index_expression 277 | field: (field_identifier) @function.builtin) 278 | (method_index_expression 279 | method: (field_identifier) @function.builtin) 280 | ] 281 | (#any-of? @function.builtin 282 | "__index" "__newindex" "__call" "__concat" "__unm" "__add" "__sub" "__mul" 283 | "__div" "__idiv" "__mod" "__pow" "__tostring" "__metatable" "__eq" "__lt" 284 | "__le" "__mode" "__gc" "__len" "__iter")) 285 | 286 | (function_call 287 | name: (dot_index_expression 288 | table: (identifier) @variable.special 289 | field: (field_identifier) @function.builtin) 290 | (#any-of? @variable.special "math" "table" "string" "coroutine" "bit32" "utf8" "os" "debug" 291 | "buffer" "vector")) 292 | 293 | ; string.match 294 | (function_call 295 | name: (dot_index_expression 296 | table: (identifier) @variable.special 297 | (#eq? @variable.special "string") 298 | field: (field_identifier) @function.builtin 299 | (#any-of? @function.builtin "find" "match" "gmatch" "gsub")) 300 | arguments: (arguments 301 | (string) 302 | (string 303 | content: (_) @string.regex) @string.regex)) 304 | 305 | ; ("string"):match 306 | (function_call 307 | name: (method_index_expression 308 | method: (field_identifier) @function.builtin 309 | (#any-of? @function.builtin "find" "match" "gmatch" "gsub")) 310 | arguments: (arguments 311 | (string 312 | content: (_) @string.regex) @string.regex)) 313 | 314 | ; TODO: Special highlight for type methods and the `types` library in type functions when it's possible 315 | ; to query descendants. 316 | 317 | ; Comments 318 | 319 | (comment) @comment 320 | 321 | (hash_bang_line) @preproc 322 | 323 | ((comment) @comment.doc 324 | (#match? @comment.doc "^[-][-][-]")) 325 | 326 | ((comment) @comment.doc 327 | (#match? @comment.doc "^[-][-](%s?)@")) 328 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zed-luau 2 | A [Zed](https://zed.dev/) extension that adds support for the [Luau scripting language](https://luau.org/). 3 | 4 | ## Installation 5 | To install zed-luau, you can use the extension menu in Zed, or clone the repository and install it 6 | as a dev extension with `zed: install dev extension`. 7 | 8 | ## Configuration 9 | This extension can be configured via your Zed `settings.json`. The default configuration looks like 10 | this: 11 | 12 | 13 | ```jsonc 14 | { 15 | "lsp": { 16 | "luau-lsp": { 17 | "settings": { 18 | // luau-lsp settings. What belongs here is specified below this entire block 19 | // of code and the contents written out are a snapshot. If it seems the snapshot 20 | // is out of date, please file an issue or PR about it. 21 | "luau-lsp": { 22 | // Files that match these globs will not be shown during auto-import 23 | "ignoreGlobs": [], 24 | "sourcemap": { 25 | // Whether Rojo sourcemap-related features are enabled 26 | "enabled": true, 27 | // Whether we should autogenerate the Rojo sourcemap by calling `rojo sourcemap` 28 | "autogenerate": true, 29 | // The project file to generate a sourcemap for 30 | "rojoProjectFile": "default.project.json", 31 | // Whether non script instances should be included in the generated sourcemap 32 | "includeNonScripts": true, 33 | // The sourcemap file name 34 | "sourcemapFile": "sourcemap.json" 35 | }, 36 | "diagnostics": { 37 | // Whether to also compute diagnostics for dependents when a file changes 38 | "includeDependents": true, 39 | // Whether to compute diagnostics for a whole workspace 40 | "workspace": false, 41 | // Whether to use expressive DM types in the diagnostics typechecker 42 | "strictDatamodelTypes": false 43 | }, 44 | "types": { 45 | // Any definition files to load globally 46 | "definitionFiles": [], 47 | // A list of globals to remove from the global scope. Accepts full libraries 48 | // or particular functions (e.g., `table` or `table.clone`) 49 | "disabledGlobals": [] 50 | }, 51 | "inlayHints": { 52 | // "none" | "literals" | "all" 53 | "parameterNames": "none", 54 | "variableTypes": false, 55 | "parameterTypes": false, 56 | "functionReturnTypes": false, 57 | "hideHintsForErrorTypes": false, 58 | "hideHintsForMatchingParameterNames": true, 59 | "typeHintMaxLength": 50, 60 | // Whether type inlay hints should be made insertable 61 | "makeInsertable": true 62 | }, 63 | "hover": { 64 | "enabled": true, 65 | "showTableKinds": false, 66 | "multilineFunctionDefinitions": false, 67 | "strictDatamodelTypes": true, 68 | "includeStringLength": true 69 | }, 70 | "completion": { 71 | "enabled": true, 72 | // Whether to automatically autocomplete end 73 | "autocompleteEnd": false, 74 | // Automatic imports configuration 75 | "imports": { 76 | // Whether we should suggest automatic imports in completions 77 | "enabled": false, 78 | // Whether services should be suggested in auto-import 79 | "suggestServices": true, 80 | // Whether requires should be suggested in auto-import 81 | "suggestRequires": true, 82 | // The style of the auto-imported require. 83 | // "Auto" | "AlwaysRelative" | "AlwaysAbsolute" 84 | "requireStyle": "Auto", 85 | "stringRequires": { 86 | // Whether to use string requires when auto-importing requires (roblox platform only) 87 | "enabled": false 88 | }, 89 | // Whether services and requires should be separated by an empty line 90 | "separateGroupsWithLine": false, 91 | // Files that match these globs will not be shown during auto-import 92 | "ignoreGlobs": [] 93 | }, 94 | // Automatically add parentheses to a function call 95 | "addParentheses": true, 96 | // If parentheses are added, include a $0 tabstop after the parentheses 97 | "addTabstopAfterParentheses": true, 98 | // If parentheses are added, fill call arguments with parameter names 99 | "fillCallArguments": true, 100 | // Whether to show non-function properties when performing a method call with a colon 101 | "showPropertiesOnMethodCall": false, 102 | // Enables the experimental fragment autocomplete system for performance improvements 103 | "enableFragmentAutocomplete": false 104 | }, 105 | "signatureHelp": { 106 | "enabled": true 107 | }, 108 | "index": { 109 | // Whether the whole workspace should be indexed. If disabled, only limited support is 110 | // available for features such as "Find All References" and "Rename" 111 | "enabled": true, 112 | // The maximum amount of files that can be indexed 113 | "maxFiles": 10000 114 | } 115 | }, 116 | "roblox": { 117 | // Whether or not Roblox-specific features should be enabled. 118 | "enabled": false, 119 | // The security level of scripts. 120 | // Must be "roblox_script", "local_user", "plugin" or "none". 121 | "security_level": "plugin", 122 | // Whether or not API documentation should be downloaded and added to luau-lsp. 123 | "download_api_documentation": true, 124 | // Whether or not definitions should be downloaded and added to luau-lsp. 125 | "download_definitions": true 126 | }, 127 | "fflags": { 128 | // Whether or not all boolean, non-experimental fflags should be enabled 129 | // by default. 130 | "enable_by_default": false, 131 | // Whether or not the new Luau type solver should be enabled. 132 | "enable_new_solver": false, 133 | // Whether or not FFlag values should be synced with Roblox's default 134 | // FFlag values. 135 | "sync": true, 136 | // FFlags that are forced to some value. 137 | "override": {} 138 | }, 139 | "binary": { 140 | // Whether or not the extension should skip searching for a binary in 141 | // your `$PATH` to use instead of installing one itself. 142 | "ignore_system_version": false, 143 | // The path to the language server binary you want to force the extension 144 | // to use. 145 | "path": null, 146 | // Additional arguments to pass to the language server. If you want to 147 | // set exactly which arguments are passed, use `lsp.luau-lsp.binary.path` 148 | // & `lsp.luau-lsp.binary.args` instead. Note that this path does not 149 | // support tilde expansion (`~/...`). 150 | "args": [] 151 | }, 152 | "plugin": { 153 | // Whether or not Roblox Studio Plugin support should be enabled. If false, the 154 | // extension will use the regular language server binary only, whereas if true, 155 | // it will use, thereby starting an HTTP server, and potentially install 156 | // 4teapo/luau-lsp-proxy as well. This is necessary for plugin support 157 | // to be possible. 158 | "enabled": false, 159 | // The port number to connect the Roblox Studio Plugin to. 160 | "port": 3667, 161 | // The path to the luau-lsp-proxy binary you want to force the extension 162 | // to use. If null, the extension tries to install it itself. 163 | "proxy_path": null 164 | }, 165 | // Additional definition file paths to pass to the language server. 166 | "definitions": [], 167 | // Additional documentation file paths to pass to the language server. 168 | "documentation": [] 169 | } 170 | } 171 | } 172 | } 173 | ``` 174 | 175 | The configuration options for `settings.luau-lsp` are shown in the `ClientConfiguration` structure 176 | [here](https://github.com/JohnnyMorganz/luau-lsp/blob/main/src/include/LSP/ClientConfiguration.hpp). 177 | For example, to enable inlay hints, you can add the following to your Zed `settings.json`: 178 | 179 | ```jsonc 180 | { 181 | "inlay_hints": { 182 | "enabled": true 183 | }, 184 | "lsp": { 185 | "luau-lsp": { 186 | "settings": { 187 | "luau-lsp": { 188 | "inlayHints": { 189 | "parameterNames": "all" 190 | } 191 | } 192 | } 193 | } 194 | } 195 | } 196 | ``` 197 | 198 | ## Rojo 199 | zed-luau does not provide Rojo support by itself. It's ergonomical to use [Zed tasks](https://zed.dev/docs/tasks) 200 | to run Rojo commands. For example: 201 | 202 | ```json 203 | [ 204 | { 205 | "label": "Serve and autogenerate sourcemap", 206 | "command": "rojo serve & rojo sourcemap --watch --include-non-scripts --output sourcemap.json" 207 | }, 208 | { 209 | "label": "Build and open", 210 | "command": "rojo build --output a.rbxl; open a.rbxl" 211 | } 212 | ] 213 | ``` 214 | 215 | To get autocompletion in `project.json` files, you can add the following to your Zed `settings.json`: 216 | 217 | ```jsonc 218 | { 219 | "lsp": { 220 | "json-language-server": { 221 | "settings": { 222 | "json": { 223 | "schemas": [ 224 | { 225 | "fileMatch": ["*.project.json"], 226 | "url": "https://raw.githubusercontent.com/rojo-rbx/vscode-rojo/refs/heads/master/schemas/project.template.schema.json" 227 | } 228 | ] 229 | } 230 | } 231 | } 232 | } 233 | } 234 | ``` 235 | 236 | ## Lune 237 | To use zed-luau with lune, follow the [Editor Setup guide](https://lune-org.github.io/docs/getting-started/4-editor-setup). 238 | The editor settings for Zed are as follows: 239 | 240 | ```jsonc 241 | { 242 | "lsp": { 243 | "luau-lsp": { 244 | "settings": { 245 | "luau-lsp": { 246 | "require": { 247 | "mode": "relativeToFile", 248 | "directoryAliases": { 249 | "@lune/": "~/.lune/.typedefs/x.y.z/" 250 | } 251 | } 252 | } 253 | } 254 | } 255 | } 256 | } 257 | ``` 258 | 259 | ## Formatting 260 | For automatically formatting your code, you can install 261 | [StyLua](https://github.com/JohnnyMorganz/StyLua), a Lua code formatter. Then, 262 | use the following settings in your Zed `settings.json`: 263 | 264 | ```jsonc 265 | { 266 | "languages": { 267 | "Luau": { 268 | "formatter": { 269 | "external": { 270 | "command": "stylua", 271 | "arguments": ["-"] 272 | } 273 | } 274 | } 275 | } 276 | } 277 | ``` 278 | 279 | ## Runnables 280 | zed-luau marks expressions of the form 281 | ```luau 282 | x("", ...) 283 | ``` 284 | where `x` is `it`, `describe`, or `test`, as runnables with the tag `luau-jest-test`, and sets 285 | `$ZED_CUSTOM_script` to the contents of the string parameter. This is helpful if you're using 286 | [jest-lua](https://github.com/jsdotlua/jest-lua) or a similar testing framework. 287 | 288 | ## Missing Features 289 | - Bytecode generation ([#20042](https://github.com/zed-industries/zed/issues/20042)) 290 | - Language-server-assisted end autocomplete 291 | -------------------------------------------------------------------------------- /src/luau.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::collections::HashMap; 3 | use std::ffi::OsStr; 4 | use std::fs; 5 | use std::path::Path; 6 | use zed::lsp::CompletionKind; 7 | use zed::settings::LspSettings; 8 | use zed::{CodeLabel, CodeLabelSpan, LanguageServerId, serde_json}; 9 | use zed_extension_api::{self as zed, Result}; 10 | 11 | mod roblox; 12 | 13 | const FFLAG_URL: &str = 14 | "https://clientsettingscdn.roblox.com/v1/settings/application?applicationName=PCDesktopClient"; 15 | const FFLAG_PREFIXES: &[&str] = &["FFlag", "FInt", "DFFlag", "DFInt"]; 16 | const FFLAG_FILE_NAME: &str = "fflags.json"; 17 | const LUAU_LSP_BINARY_DIR_NAME: &str = "luau-lsp-binaries"; 18 | const PROXY_BINARY_DIR_NAME: &str = "proxy-binaries"; 19 | 20 | #[derive(Debug, Deserialize)] 21 | #[serde(default)] 22 | struct Settings { 23 | roblox: RobloxSettings, 24 | fflags: FFlagsSettings, 25 | binary: BinarySettings, 26 | plugin: PluginSettings, 27 | definitions: Vec, 28 | documentation: Vec, 29 | } 30 | 31 | impl Default for Settings { 32 | fn default() -> Self { 33 | Settings { 34 | roblox: Default::default(), 35 | fflags: Default::default(), 36 | binary: Default::default(), 37 | plugin: Default::default(), 38 | definitions: Default::default(), 39 | documentation: Default::default(), 40 | } 41 | } 42 | } 43 | 44 | #[derive(Debug, Deserialize)] 45 | #[serde(default)] 46 | struct RobloxSettings { 47 | enabled: bool, 48 | security_level: SecurityLevel, 49 | download_api_documentation: bool, 50 | download_definitions: bool, 51 | } 52 | 53 | impl Default for RobloxSettings { 54 | fn default() -> Self { 55 | Self { 56 | enabled: false, 57 | security_level: SecurityLevel::Plugin, 58 | download_api_documentation: true, 59 | download_definitions: true, 60 | } 61 | } 62 | } 63 | 64 | #[derive(Debug, Deserialize)] 65 | #[serde(rename_all = "snake_case")] 66 | enum SecurityLevel { 67 | RobloxScript, 68 | LocalUser, 69 | Plugin, 70 | None, 71 | } 72 | 73 | #[derive(Debug, Deserialize)] 74 | #[serde(default)] 75 | struct FFlagsSettings { 76 | enable_by_default: bool, 77 | enable_new_solver: bool, 78 | sync: bool, 79 | #[serde(rename = "override")] 80 | overrides: HashMap, 81 | } 82 | 83 | impl Default for FFlagsSettings { 84 | fn default() -> Self { 85 | Self { 86 | enable_by_default: false, 87 | enable_new_solver: false, 88 | sync: true, 89 | overrides: Default::default(), 90 | } 91 | } 92 | } 93 | 94 | #[derive(Debug, Deserialize)] 95 | #[serde(default)] 96 | struct BinarySettings { 97 | ignore_system_version: bool, 98 | path: Option, 99 | args: Vec, 100 | } 101 | 102 | impl Default for BinarySettings { 103 | fn default() -> Self { 104 | Self { 105 | ignore_system_version: false, 106 | path: None, 107 | args: Default::default(), 108 | } 109 | } 110 | } 111 | 112 | #[derive(Debug, Deserialize)] 113 | #[serde(default)] 114 | struct PluginSettings { 115 | enabled: bool, 116 | port: u16, 117 | proxy_path: Option, 118 | } 119 | 120 | impl Default for PluginSettings { 121 | fn default() -> Self { 122 | Self { 123 | enabled: false, 124 | port: 3667, 125 | proxy_path: None, 126 | } 127 | } 128 | } 129 | 130 | struct LuauExtension { 131 | cached_binary_path: Option, 132 | cached_proxy_path: Option, 133 | } 134 | 135 | fn is_file(path: &str) -> bool { 136 | fs::metadata(path).map_or(false, |stat| stat.is_file()) 137 | } 138 | 139 | fn is_dir(path: &str) -> bool { 140 | fs::metadata(path).map_or(false, |stat| stat.is_dir()) 141 | } 142 | 143 | fn get_extension_settings(settings_val: Option) -> Result { 144 | let Some(mut settings_val) = settings_val else { 145 | return Ok(Settings::default()); 146 | }; 147 | 148 | let Some(settings) = settings_val.as_object_mut() else { 149 | return Err("invalid luau-lsp settings: `settings` must be an object, but isn't.".into()); 150 | }; 151 | 152 | let value = settings.remove("ext").unwrap_or(settings_val); 153 | 154 | serde_path_to_error::deserialize(value).map_err(|e| e.to_string()) 155 | } 156 | 157 | fn download_fflags() -> Result<()> { 158 | zed::download_file( 159 | FFLAG_URL, 160 | FFLAG_FILE_NAME, 161 | zed::DownloadedFileType::Uncompressed, 162 | ) 163 | .map_err(|e| format!("failed to download file: {e}"))?; 164 | Ok(()) 165 | } 166 | 167 | struct BinaryPath { 168 | path: String, 169 | is_extension_owned: bool, 170 | } 171 | 172 | impl LuauExtension { 173 | fn language_server_binary_path( 174 | &mut self, 175 | language_server_id: &LanguageServerId, 176 | worktree: &zed::Worktree, 177 | settings: &Settings, 178 | ) -> Result { 179 | if let Some(path) = &settings.binary.path { 180 | return Ok(BinaryPath { 181 | path: path.clone(), 182 | is_extension_owned: false, 183 | }); 184 | } 185 | 186 | if !settings.binary.ignore_system_version { 187 | if let Some(path) = worktree.which("luau-lsp") { 188 | return Ok(BinaryPath { 189 | path, 190 | is_extension_owned: false, 191 | }); 192 | } 193 | } 194 | 195 | if let Some(path) = &self.cached_binary_path { 196 | if is_file(path) { 197 | return Ok(BinaryPath { 198 | path: path.clone(), 199 | is_extension_owned: true, 200 | }); 201 | } 202 | } 203 | 204 | zed::set_language_server_installation_status( 205 | &language_server_id, 206 | &zed::LanguageServerInstallationStatus::CheckingForUpdate, 207 | ); 208 | let release = zed::latest_github_release( 209 | "JohnnyMorganz/luau-lsp", 210 | zed::GithubReleaseOptions { 211 | require_assets: true, 212 | pre_release: false, 213 | }, 214 | )?; 215 | 216 | let (platform, arch) = zed::current_platform(); 217 | 218 | let asset_name = match platform { 219 | zed::Os::Mac => "luau-lsp-macos.zip", 220 | zed::Os::Windows => "luau-lsp-win64.zip", 221 | zed::Os::Linux => match arch { 222 | zed::Architecture::Aarch64 => "luau-lsp-linux-arm64.zip", 223 | _ => "luau-lsp-linux-x86_64.zip", 224 | }, 225 | }; 226 | 227 | let asset = release 228 | .assets 229 | .iter() 230 | .find(|asset| asset.name == asset_name) 231 | .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?; 232 | 233 | let dir_name = format!("luau-lsp-{}", release.version); 234 | let version_dir = format!("{LUAU_LSP_BINARY_DIR_NAME}/{dir_name}"); 235 | let binary_path = format!( 236 | "{version_dir}/luau-lsp{}", 237 | match platform { 238 | zed::Os::Mac | zed::Os::Linux => "", 239 | zed::Os::Windows => ".exe", 240 | } 241 | ); 242 | 243 | if !is_dir(LUAU_LSP_BINARY_DIR_NAME) { 244 | fs::create_dir(LUAU_LSP_BINARY_DIR_NAME) 245 | .map_err(|e| format!("failed to create directory for the luau-lsp binary: {e}"))?; 246 | } 247 | 248 | if !is_file(&binary_path) { 249 | zed::set_language_server_installation_status( 250 | &language_server_id, 251 | &zed::LanguageServerInstallationStatus::Downloading, 252 | ); 253 | 254 | zed::download_file( 255 | &asset.download_url, 256 | &version_dir, 257 | zed::DownloadedFileType::Zip, 258 | ) 259 | .map_err(|e| format!("failed to download file: {e}"))?; 260 | 261 | zed::make_file_executable(&binary_path)?; 262 | 263 | let entries = fs::read_dir(LUAU_LSP_BINARY_DIR_NAME) 264 | .map_err(|e| format!("failed to list luau-lsp binary directory {e}"))?; 265 | for entry in entries { 266 | let entry = entry 267 | .map_err(|e| format!("failed to load luau-lsp binary directory entry {e}"))?; 268 | if entry.file_name().to_str() != Some(&dir_name) { 269 | fs::remove_dir_all(&entry.path()).ok(); 270 | } 271 | } 272 | } 273 | 274 | self.cached_binary_path = Some(binary_path.clone()); 275 | 276 | Ok(BinaryPath { 277 | path: binary_path, 278 | is_extension_owned: true, 279 | }) 280 | } 281 | 282 | fn proxy_binary_path( 283 | &mut self, 284 | language_server_id: &LanguageServerId, 285 | settings: &Settings, 286 | ) -> Result { 287 | if let Some(path) = &settings.plugin.proxy_path { 288 | return Ok(path.clone()); 289 | } 290 | 291 | zed::set_language_server_installation_status( 292 | &language_server_id, 293 | &zed::LanguageServerInstallationStatus::CheckingForUpdate, 294 | ); 295 | 296 | // We version pin the proxy so that we don't need to worry about backwards compatibility for it. 297 | let release = zed::github_release_by_tag_name("4teapo/luau-lsp-proxy", "v0.1.0")?; 298 | 299 | let (platform, arch) = zed::current_platform(); 300 | 301 | let asset_name = format!( 302 | "luau-lsp-proxy-{version}-{os}-{arch}.zip", 303 | version = { 304 | let mut chars = release.version.chars(); 305 | chars.next(); 306 | chars.as_str() 307 | }, 308 | os = match platform { 309 | zed::Os::Mac => "macos", 310 | zed::Os::Windows => "windows", 311 | zed::Os::Linux => "linux", 312 | }, 313 | arch = match arch { 314 | zed::Architecture::Aarch64 => "aarch64", 315 | _ => "x86_64", 316 | }, 317 | ); 318 | 319 | let asset = release 320 | .assets 321 | .iter() 322 | .find(|asset| asset.name == asset_name) 323 | .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?; 324 | 325 | let dir_name = format!("luau-lsp-proxy-{}", release.version); 326 | let version_dir = format!("{PROXY_BINARY_DIR_NAME}/{dir_name}"); 327 | let binary_path = format!( 328 | "{version_dir}/luau-lsp-proxy{}", 329 | match platform { 330 | zed::Os::Mac | zed::Os::Linux => "", 331 | zed::Os::Windows => ".exe", 332 | } 333 | ); 334 | 335 | if !is_dir(PROXY_BINARY_DIR_NAME) { 336 | fs::create_dir(PROXY_BINARY_DIR_NAME) 337 | .map_err(|e| format!("failed to create directory for the proxy binary: {e}"))?; 338 | } 339 | 340 | if !is_file(&binary_path) { 341 | zed::set_language_server_installation_status( 342 | &language_server_id, 343 | &zed::LanguageServerInstallationStatus::Downloading, 344 | ); 345 | 346 | zed::download_file( 347 | &asset.download_url, 348 | &version_dir, 349 | zed::DownloadedFileType::Zip, 350 | ) 351 | .map_err(|e| format!("failed to download file: {e}"))?; 352 | 353 | zed::make_file_executable(&binary_path)?; 354 | 355 | let entries = fs::read_dir(PROXY_BINARY_DIR_NAME) 356 | .map_err(|e| format!("failed to list proxy binary directory {e}"))?; 357 | for entry in entries { 358 | let entry = entry 359 | .map_err(|e| format!("failed to load proxy binary directory entry {e}"))?; 360 | if entry.file_name().to_str() != Some(&dir_name) { 361 | fs::remove_dir_all(&entry.path()).ok(); 362 | } 363 | } 364 | } 365 | 366 | self.cached_proxy_path = Some(binary_path.clone()); 367 | 368 | Ok(binary_path) 369 | } 370 | } 371 | 372 | impl zed::Extension for LuauExtension { 373 | fn new() -> Self { 374 | // Try deleting files for definitions, docs & fflags to make sure they are downloaded again 375 | // later, keeping them up to date. 376 | fs::remove_file(FFLAG_FILE_NAME).ok(); 377 | fs::remove_file(roblox::API_DOCS_FILE_NAME).ok(); 378 | fs::remove_file(roblox::get_definitions_file_for_level( 379 | roblox::SECURITY_LEVEL_ROBLOX_SCRIPT, 380 | )) 381 | .ok(); 382 | fs::remove_file(roblox::get_definitions_file_for_level( 383 | roblox::SECURITY_LEVEL_LOCAL_USER, 384 | )) 385 | .ok(); 386 | fs::remove_file(roblox::get_definitions_file_for_level( 387 | roblox::SECURITY_LEVEL_PLUGIN, 388 | )) 389 | .ok(); 390 | fs::remove_file(roblox::get_definitions_file_for_level( 391 | roblox::SECURITY_LEVEL_NONE, 392 | )) 393 | .ok(); 394 | Self { 395 | cached_binary_path: None, 396 | cached_proxy_path: None, 397 | } 398 | } 399 | 400 | fn language_server_command( 401 | &mut self, 402 | language_server_id: &LanguageServerId, 403 | worktree: &zed::Worktree, 404 | ) -> Result { 405 | let lsp_settings = match LspSettings::for_worktree(language_server_id.as_ref(), worktree) { 406 | Ok(v) => v, 407 | Err(e) => return Err(e), 408 | }; 409 | 410 | let settings = get_extension_settings(lsp_settings.settings)?; 411 | 412 | let binary_path = 413 | self.language_server_binary_path(language_server_id, worktree, &settings)?; 414 | 415 | let current_dir = std::env::current_dir().unwrap(); 416 | let current_dir_str = current_dir.display(); 417 | 418 | fn is_path_absolute(path: &str) -> bool { 419 | let (platform, _) = zed::current_platform(); 420 | match platform { 421 | // We need to handle Windows manually because of our UNIX-based WASM environment 422 | zed::Os::Windows => { 423 | let mut chars = path.chars(); 424 | match (chars.next(), chars.next(), chars.next()) { 425 | (Some(drive), Some(':'), Some(sep)) => { 426 | drive.is_ascii_alphabetic() && (sep == '\\' || sep == '/') 427 | } 428 | // UNC path 429 | _ => path.starts_with("//") || path.starts_with("\\\\"), 430 | } 431 | } 432 | _ => Path::new(OsStr::new(path)).is_absolute(), 433 | } 434 | } 435 | 436 | let mut args: Vec = Vec::new(); 437 | if settings.plugin.enabled { 438 | args.push(settings.plugin.port.to_string()); 439 | if binary_path.is_extension_owned { 440 | args.push(format!("{}/{}", current_dir_str, binary_path.path.clone()).into()); 441 | } else { 442 | args.push(binary_path.path.clone().into()); 443 | } 444 | } 445 | args.push("lsp".into()); 446 | 447 | // Handle fflag settings. 448 | { 449 | if !settings.fflags.enable_by_default { 450 | args.push("--no-flags-enabled".into()); 451 | } 452 | 453 | let mut fflags = HashMap::new(); 454 | 455 | if settings.fflags.sync { 456 | if !is_file(FFLAG_FILE_NAME) { 457 | download_fflags()?; 458 | } 459 | let as_str = fs::read_to_string(FFLAG_FILE_NAME) 460 | .map_err(|e| format!("failed to read fflags.json: {e}"))?; 461 | let json: serde_json::Value = serde_json::from_str(&as_str) 462 | .map_err(|e| format!("failed to parse fflags.json: {e}"))?; 463 | let Some(json_map) = json.as_object() else { 464 | return Err("failed to sync fflags: error when parsing fetched fflags: fflags must be an object, but isn't.".into()); 465 | }; 466 | let Some(app_settings_val) = json_map.get("applicationSettings") else { 467 | return Err("failed to sync fflags: error when reading parsed fflags: json.applicationSettings must exist, but doesn't.".into()); 468 | }; 469 | let Some(app_settings) = app_settings_val.as_object() else { 470 | return Err("failed to sync fflags: error when reading parsed fflags: json.applicationSettings must be an object, but isn't.".into()); 471 | }; 472 | for (name, value) in app_settings.iter() { 473 | let Some(value) = value.as_str() else { 474 | return Err("failed to sync fflags: error when reading parsed fflags: all values in json.applicationSettings must be strings, but one or more aren't.".into()); 475 | }; 476 | for prefix in FFLAG_PREFIXES { 477 | if name.starts_with(&format!("{prefix}Luau")) { 478 | fflags.insert(name[prefix.len()..].into(), value.to_string()); 479 | break; 480 | } 481 | } 482 | } 483 | } 484 | 485 | for (name, value) in settings.fflags.overrides.iter() { 486 | if name.len() == 0 || value.len() == 0 { 487 | return Err("failed to apply fflag overrides: all overrides must have a non-empty name and value.".into()); 488 | } 489 | fflags.insert(name.clone(), value.to_string()); 490 | } 491 | 492 | if settings.fflags.enable_new_solver { 493 | fflags.insert("LuauSolverV2".to_string(), "true".to_string()); 494 | fflags.insert( 495 | "LuauNewSolverPopulateTableLocations".to_string(), 496 | "true".to_string(), 497 | ); 498 | fflags.insert( 499 | "LuauNewSolverPrePopulateClasses".to_string(), 500 | "true".to_string(), 501 | ); 502 | } 503 | 504 | for (name, value) in fflags.iter() { 505 | args.push(format!("--flag:{}={}", name, value).into()); 506 | } 507 | } 508 | 509 | if settings.roblox.enabled { 510 | if settings.roblox.download_api_documentation { 511 | if !is_file(roblox::API_DOCS_FILE_NAME) { 512 | roblox::download_api_docs()?; 513 | } 514 | args.push( 515 | format!("--docs={}/{}", ¤t_dir_str, roblox::API_DOCS_FILE_NAME).into(), 516 | ); 517 | } 518 | 519 | if settings.roblox.download_definitions { 520 | let security_level = match settings.roblox.security_level { 521 | SecurityLevel::None => roblox::SECURITY_LEVEL_NONE, 522 | SecurityLevel::RobloxScript => roblox::SECURITY_LEVEL_ROBLOX_SCRIPT, 523 | SecurityLevel::LocalUser => roblox::SECURITY_LEVEL_LOCAL_USER, 524 | SecurityLevel::Plugin => roblox::SECURITY_LEVEL_PLUGIN, 525 | }; 526 | 527 | let definitions_file_name = roblox::get_definitions_file_for_level(security_level); 528 | 529 | if !is_file(&definitions_file_name) { 530 | roblox::download_definitions(security_level)?; 531 | } 532 | args.push( 533 | format!( 534 | "--definitions={}/{}", 535 | ¤t_dir_str, definitions_file_name 536 | ) 537 | .into(), 538 | ); 539 | } 540 | } 541 | 542 | // Handle documentation and definition settings. 543 | // Happens after handling Roblox settings because we want these to be added after the 544 | // Roblox definition files are, because otherwise they can't depend on the Roblox types. 545 | { 546 | fn get_prefix<'a>(path: &str, proj_root_str: &'a str) -> &'a str { 547 | match is_path_absolute(path) { 548 | true => "", 549 | false => proj_root_str, 550 | } 551 | } 552 | 553 | let proj_root_str = &format!("{}/", worktree.root_path()); 554 | 555 | for def in &settings.definitions { 556 | let prefix = get_prefix(&def, &proj_root_str); 557 | args.push(format!("--definitions={prefix}{def}").into()); 558 | } 559 | 560 | for doc in &settings.documentation { 561 | let prefix = get_prefix(&doc, &proj_root_str); 562 | args.push(format!("--docs={prefix}{doc}").into()); 563 | } 564 | } 565 | 566 | for arg in &settings.binary.args { 567 | args.push(arg.into()); 568 | } 569 | 570 | let command = if settings.plugin.enabled { 571 | self.proxy_binary_path(language_server_id, &settings)? 572 | } else { 573 | binary_path.path.clone() 574 | }; 575 | 576 | Ok(zed::Command { 577 | command, 578 | args, 579 | env: Default::default(), 580 | }) 581 | } 582 | 583 | fn language_server_initialization_options( 584 | &mut self, 585 | language_server_id: &LanguageServerId, 586 | worktree: &zed_extension_api::Worktree, 587 | ) -> Result> { 588 | let lsp_settings = LspSettings::for_worktree(language_server_id.as_ref(), worktree)?; 589 | let initialization_options = lsp_settings.initialization_options; 590 | Ok(initialization_options) 591 | } 592 | 593 | fn label_for_completion( 594 | &self, 595 | _language_server_id: &LanguageServerId, 596 | completion: zed::lsp::Completion, 597 | ) -> Option { 598 | match completion.kind? { 599 | CompletionKind::Method | CompletionKind::Function => { 600 | let name_len = completion.label.find('(').unwrap_or(completion.label.len()); 601 | Some(CodeLabel { 602 | spans: vec![CodeLabelSpan::code_range(0..completion.label.len())], 603 | filter_range: (0..name_len).into(), 604 | code: completion.label, 605 | }) 606 | } 607 | CompletionKind::Field => Some(CodeLabel { 608 | spans: vec![CodeLabelSpan::literal( 609 | completion.label.clone(), 610 | Some("property".into()), 611 | )], 612 | filter_range: (0..completion.label.len()).into(), 613 | code: Default::default(), 614 | }), 615 | _ => None, 616 | } 617 | } 618 | 619 | fn language_server_workspace_configuration( 620 | &mut self, 621 | language_server_id: &zed::LanguageServerId, 622 | _worktree: &zed::Worktree, 623 | ) -> Result> { 624 | let settings = LspSettings::for_worktree(language_server_id.as_ref(), _worktree) 625 | .ok() 626 | .and_then(|lsp_settings| lsp_settings.settings.clone()) 627 | .unwrap_or_default(); 628 | Ok(Some(serde_json::json!(settings))) 629 | } 630 | } 631 | 632 | zed::register_extension!(LuauExtension); 633 | --------------------------------------------------------------------------------