diff options
58 files changed, 4249 insertions, 1174 deletions
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a1f50f6..0c6547e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,6 +38,7 @@ jobs: run: | mkdir dist mv ols dist/ols-arm64-darwin + mv odinfmt dist/odinfmt-arm64-darwin - name: Upload artifacts uses: actions/upload-artifact@v4 with: @@ -73,6 +74,7 @@ jobs: run: | mkdir dist mv ols dist/ols-x86_64-darwin + mv odinfmt dist/odinfmt-x86_64-darwin - name: Upload artifacts uses: actions/upload-artifact@v4 with: @@ -108,6 +110,7 @@ jobs: run: | mkdir dist mv ols dist/ols-x86_64-unknown-linux-gnu + mv odinfmt dist/odinfmt-x86_64-unknown-linux-gnu - name: Upload artifacts uses: actions/upload-artifact@v4 with: @@ -145,6 +148,7 @@ jobs: run: | mkdir dist mv ols dist/ols-arm64-unknown-linux-gnu + mv odinfmt dist/odinfmt-arm64-unknown-linux-gnu - name: Upload artifacts uses: actions/upload-artifact@v4 with: @@ -181,9 +185,11 @@ jobs: run: | mkdir dist move ols.exe dist/ + move odinfmt.exe dist/ move builtin dist/ cd dist ren ols.exe ols-x86_64-pc-windows-msvc.exe + ren odinfmt.exe odinfmt-x86_64-pc-windows-msvc.exe - name: Upload artifacts uses: actions/upload-artifact@v4 with: @@ -230,24 +236,24 @@ jobs: - run: | ls -al ./dist cd dist - zip -r ols-x86_64-pc-windows-msvc.zip ols-x86_64-pc-windows-msvc.exe builtin - rm ols-x86_64-pc-windows-msvc.exe + zip -r ols-x86_64-pc-windows-msvc.zip ols-x86_64-pc-windows-msvc.exe odinfmt-x86_64-pc-windows-msvc.exe builtin + rm ols-x86_64-pc-windows-msvc.exe odinfmt-x86_64-pc-windows-msvc.exe - chmod +x ols-x86_64-unknown-linux-gnu - zip -r ols-x86_64-unknown-linux-gnu.zip ols-x86_64-unknown-linux-gnu builtin - rm ols-x86_64-unknown-linux-gnu + chmod +x ols-x86_64-unknown-linux-gnu odinfmt-x86_64-unknown-linux-gnu + zip -r ols-x86_64-unknown-linux-gnu.zip ols-x86_64-unknown-linux-gnu odinfmt-x86_64-unknown-linux-gnu builtin + rm ols-x86_64-unknown-linux-gnu odinfmt-x86_64-unknown-linux-gnu - chmod +x ols-arm64-unknown-linux-gnu - zip -r ols-arm64-unknown-linux-gnu.zip ols-arm64-unknown-linux-gnu builtin - rm ols-arm64-unknown-linux-gnu + chmod +x ols-arm64-unknown-linux-gnu odinfmt-arm64-unknown-linux-gnu + zip -r ols-arm64-unknown-linux-gnu.zip ols-arm64-unknown-linux-gnu odinfmt-arm64-unknown-linux-gnu builtin + rm ols-arm64-unknown-linux-gnu odinfmt-arm64-unknown-linux-gnu - chmod +x ols-x86_64-darwin - zip -r ols-x86_64-darwin.zip ols-x86_64-darwin builtin - rm ols-x86_64-darwin + chmod +x ols-x86_64-darwin odinfmt-x86_64-darwin + zip -r ols-x86_64-darwin.zip ols-x86_64-darwin odinfmt-x86_64-darwin builtin + rm ols-x86_64-darwin odinfmt-x86_64-darwin - chmod +x ols-arm64-darwin - zip -r ols-arm64-darwin.zip ols-arm64-darwin builtin - rm ols-arm64-darwin + chmod +x ols-arm64-darwin odinfmt-arm64-darwin + zip -r ols-arm64-darwin.zip ols-arm64-darwin odinfmt-arm64-darwin builtin + rm ols-arm64-darwin odinfmt-arm64-darwin rm -rf builtin - name: Publish Release @@ -36,6 +36,8 @@ cd ols ./odinfmt.sh ``` +In order for `ols` to find symbols for builtin types and procedures, the `builtin` folder in the repo needs to be located next to the `ols` binary. + ### Configuration In order for the language server to index your files, it must know about your collections. @@ -59,9 +61,10 @@ Example of `ols.json`: "enable_snippets": true, "profile": "default", "profiles": [ - { "name": "default", "checker_path": ["src"]}, - { "name": "linux_profile", "os": "linux", "checker_path": ["src/main.odin"]}, - { "name": "windows_profile", "os": "windows", "checker_path": ["src"]} + { "name": "default", "checker_path": ["src"], "defines": { "ODIN_DEBUG": "false" }}, + { "name": "linux_profile", "os": "linux", "checker_path": ["src/main.odin"], "defines": { "ODIN_DEBUG": "false" }}, + { "name": "mac_profile", "os": "darwin", "arch": "arm64", "defines": { "ODIN_DEBUG": "false" }}, + { "name": "windows_profile", "os": "windows", "checker_path": ["src"], "defines": { "ODIN_DEBUG": "false" }} ] } ``` @@ -76,6 +79,8 @@ Options: - `enable_fake_methods`: Turn on fake methods completion. This is currently highly experimental. +- `enable_overload_resolution`: Enable go-to-definition to resolve overloaded procedures from procedure groups based on call arguments. + - `enable_references`: Turns on finding references for a symbol. _(Enabled by default)_ - `enable_document_highlights`: Turns on highlighting of symbol references in file. _(Enabled by default)_ @@ -100,6 +105,10 @@ Options: - `enable_auto_import`: Automatically import packages that aren't in your import on completion. +- `enable_comp_lit_signature_help`: Provide signature help for comp lits such as when instantiating structs. Will not display correctly on some editors such as vscode. + +- `enable_comp_lit_signature_help_use_docs`: Put signature help for comp lits in the documentation. This will allow it to be rendered nicely using markdown in editors that render the label without colour on one line. + - `odin_command`: Specify the location to your Odin executable, rather than relying on the environment path. - `odin_root_override`: Allows you to specify a custom `ODIN_ROOT` that `ols` will use to look for `odin` core libraries when implementing custom runtimes. @@ -110,7 +119,7 @@ Options: - `profile`: What profile to currently use. -- `profiles`: List of different profiles that describe the environment ols is running under. +- `profiles`: List of different profiles that describe the environment ols is running under. This allows you to define different operating systems, architectures and defines for `ols` to use during development, easily switching between them using the `profile` configuration. ### Odinfmt configurations @@ -155,6 +164,10 @@ Options: - `space_single_line_blocks`: Put spaces around braces of single-line blocks: `{return 0}` => `{ return 0 }` +- `align_struct_fields`: Align the types of struct fields so they all start at the same column. + +- `align_struct_values`: Align the values of struct fields when assigning a struct value to a variable so they all start at the same column. + ## Features Support Language server features: @@ -11,12 +11,25 @@ if "%1" == "CI" ( set "PATH=%cd%\Odin;!PATH!" odin test tests -collection:src=src -define:ODIN_TEST_THREADS=1 - if %errorlevel% neq 0 exit /b 1 + if errorlevel 1 exit /b 1 odin build src\ -collection:src=src -out:ols.exe -o:speed -no-bounds-check -extra-linker-flags:"/STACK:4000000,2000000" -define:VERSION=%OLS_VERSION% + if errorlevel 1 exit /b 1 + pushd . call "tools/odinfmt/tests.bat" - if %errorlevel% neq 0 exit /b 1 + if errorlevel 1 ( + popd + exit /b 1 + ) + popd + + odin build tools\odinfmt\main.odin -file -collection:src=src -out:odinfmt.exe -o:speed -no-bounds-check -extra-linker-flags:"/STACK:4000000,2000000" + if errorlevel 1 exit /b 1 ) else ( odin build src\ -collection:src=src -out:ols.exe -o:speed -no-bounds-check -extra-linker-flags:"/STACK:4000000,2000000" -define:VERSION=%OLS_VERSION% + if errorlevel 1 exit /b 1 + + odin build tools\odinfmt\main.odin -file -collection:src=src -out:odinfmt.exe -o:speed -no-bounds-check -extra-linker-flags:"/STACK:4000000,2000000" + if errorlevel 1 exit /b 1 ) @@ -38,5 +38,8 @@ then export PATH=$PATH:$PWD/Odin fi - +echo "Building ols" odin build src/ -show-timings -collection:src=src -out:ols -no-bounds-check -o:speed -define:VERSION=$OLS_VERSION $@ + +echo "Building odinfmt" +odin build tools/odinfmt/main.odin -file -show-timings -collection:src=src -out:odinfmt -no-bounds-check -o:speed $@ diff --git a/misc/odinfmt.schema.json b/misc/odinfmt.schema.json index 6c82dde..f30c999 100644 --- a/misc/odinfmt.schema.json +++ b/misc/odinfmt.schema.json @@ -70,6 +70,16 @@ "type": "boolean", "default": false, "description": "Put spaces around braces of single-line blocks: `{return 0}` => `{ return 0 }`" + }, + "align_struct_fields": { + "type": "boolean", + "default": true, + "description": "Align the types of struct fields so they all start at the same column" + }, + "align_struct_values": { + "type": "boolean", + "default": true, + "description": "Align the values of struct fields when assigning a struct value to a variable so they all start at the same column" } }, "required": [] diff --git a/misc/ols.schema.json b/misc/ols.schema.json index c3ad997..81bc0cd 100644 --- a/misc/ols.schema.json +++ b/misc/ols.schema.json @@ -87,11 +87,26 @@ "description": "Turn on fake methods completion.", "default": false }, + "enable_overload_resolution": { + "type": "boolean", + "description": "Enable go-to-definition to resolve overloaded procedures from procedure groups based on call arguments.", + "default": false + }, "enable_document_links": { "type": "boolean", "description": "Follow links when opening documentation.", "default": true }, + "enable_comp_lit_signature_help": { + "type": "boolean", + "description": "Provide signature help for comp lits such as when instantiating structs. Will not display correctly on some editors such as vscode.", + "default": false + }, + "enable_comp_lit_signature_help_use_docs": { + "type": "boolean", + "description": "Put signature help for comp lits in the documentation. This will allow it to be rendered nicely using markdown in editors that render the label without colour on one line.", + "default": false + }, "disable_parser_errors": { "type": "boolean" }, "verbose": { "type": "boolean", @@ -128,6 +143,10 @@ "type": "string", "description": "The operating system for the profile." }, + "arch": { + "type": "string", + "description": "The architecture for the profile." + }, "checker_path": { "type": "array", "description": "List of paths where to run the checker.", @@ -141,6 +160,13 @@ "items": { "type": "string" } + }, + "defines": { + "type": "object", + "description": "Key-value pairs of defines. Used for evaluating when expressions, for example `ODIN_DEBUG`. The value must be a string.", + "additionalProperties": { + "type": "string" + } } }, "required": ["name", "checker_path"], diff --git a/src/common/config.odin b/src/common/config.odin index 4aaefac..f2ae68f 100644 --- a/src/common/config.odin +++ b/src/common/config.odin @@ -10,43 +10,46 @@ ConfigProfile :: struct { } Config :: struct { - workspace_folders: [dynamic]WorkspaceFolder, - completion_support_md: bool, - hover_support_md: bool, - signature_offset_support: bool, - collections: map[string]string, - running: bool, - verbose: bool, - enable_format: bool, - enable_hover: bool, - enable_document_symbols: bool, - enable_semantic_tokens: bool, - enable_unused_imports_reporting: bool, - enable_inlay_hints_params: bool, - enable_inlay_hints_default_params: bool, - enable_inlay_hints_implicit_return: bool, - enable_procedure_context: bool, - enable_snippets: bool, - enable_references: bool, - enable_document_highlights: bool, - enable_label_details: bool, - enable_std_references: bool, - enable_import_fixer: bool, - enable_fake_method: bool, - enable_procedure_snippet: bool, - enable_checker_only_saved: bool, - enable_auto_import: bool, - enable_completion_matching: bool, - enable_document_links: bool, - disable_parser_errors: bool, - thread_count: int, - file_log: bool, - odin_command: string, - odin_root_override: string, - checker_args: string, - checker_targets: []string, - client_name: string, - profile: ConfigProfile, + workspace_folders: [dynamic]WorkspaceFolder, + completion_support_md: bool, + hover_support_md: bool, + signature_offset_support: bool, + collections: map[string]string, + running: bool, + verbose: bool, + enable_format: bool, + enable_hover: bool, + enable_document_symbols: bool, + enable_semantic_tokens: bool, + enable_unused_imports_reporting: bool, + enable_inlay_hints_params: bool, + enable_inlay_hints_default_params: bool, + enable_inlay_hints_implicit_return: bool, + enable_procedure_context: bool, + enable_snippets: bool, + enable_references: bool, + enable_document_highlights: bool, + enable_label_details: bool, + enable_std_references: bool, + enable_import_fixer: bool, + enable_fake_method: bool, + enable_overload_resolution: bool, + enable_procedure_snippet: bool, + enable_checker_only_saved: bool, + enable_auto_import: bool, + enable_completion_matching: bool, + enable_document_links: bool, + enable_comp_lit_signature_help: bool, + enable_comp_lit_signature_help_use_docs: bool, + disable_parser_errors: bool, + thread_count: int, + file_log: bool, + odin_command: string, + odin_root_override: string, + checker_args: string, + checker_targets: []string, + client_name: string, + profile: ConfigProfile, } config: Config diff --git a/src/common/uri.odin b/src/common/uri.odin index 32c32e0..ece873e 100644 --- a/src/common/uri.odin +++ b/src/common/uri.odin @@ -45,7 +45,7 @@ parse_uri :: proc(value: string, allocator: mem.Allocator) -> (Uri, bool) { //Note(Daniel, Again some really incomplete and scuffed uri writer) create_uri :: proc(path: string, allocator: mem.Allocator) -> Uri { - path_forward, _ := filepath.to_slash(path, context.temp_allocator) + path_forward, _ := filepath.replace_path_separators(path, '/', context.temp_allocator) builder := strings.builder_make(allocator) diff --git a/src/common/util.odin b/src/common/util.odin index 974fe6b..f19bb0e 100644 --- a/src/common/util.odin +++ b/src/common/util.odin @@ -5,9 +5,7 @@ import "core:fmt" import "core:log" import "core:mem" import "core:os" -import "core:os/os2" import "core:path/filepath" -import "core:path/slashpath" import "core:strings" import "core:time" @@ -19,15 +17,12 @@ when ODIN_OS == .Windows { delimiter :: ":" } -//TODO(daniel): This is temporary and should not be needed after os2 -File_Mode_User_Executable :: os.File_Mode(1 << 8) - lookup_in_path :: proc(name: string) -> (string, bool) { path := os.get_env("PATH", context.temp_allocator) for directory in strings.split_iterator(&path, delimiter) { when ODIN_OS == .Windows { - possibility := filepath.join( + possibility, _ := filepath.join( elems = {directory, fmt.tprintf("%v.exe", name)}, allocator = context.temp_allocator, ) @@ -35,11 +30,11 @@ lookup_in_path :: proc(name: string) -> (string, bool) { return possibility, true } } else { - possibility := filepath.join(elems = {directory, name}, allocator = context.temp_allocator) + possibility, _ := filepath.join(elems = {directory, name}, allocator = context.temp_allocator) possibility = resolve_home_dir(possibility, context.temp_allocator) if os.exists(possibility) { if info, err := os.stat(possibility, context.temp_allocator); - err == os.ERROR_NONE && (File_Mode_User_Executable & info.mode) != 0 { + err == os.ERROR_NONE && .Execute_User in info.mode { return possibility, true } } @@ -66,7 +61,8 @@ resolve_home_dir :: proc( return path, false } - return filepath.join({home, path[1:]}, allocator), true + path, _ := filepath.join({home, path[1:]}, allocator) + return path, true } else if strings.has_prefix(path, "$HOME") { home := os.get_env("HOME", context.temp_allocator) if home == "" { @@ -74,13 +70,14 @@ resolve_home_dir :: proc( return path, false } - return filepath.join({home, path[5:]}, allocator), true + path, _ := filepath.join({home, path[5:]}, allocator) + return path, true } return path, false } } - FILE :: struct {} +FILE :: struct {} when ODIN_OS == .Darwin || ODIN_OS == .FreeBSD || ODIN_OS == .Linux || ODIN_OS == .NetBSD { run_executable :: proc(command: string, stdout: ^[]byte) -> (u32, bool, []byte) { @@ -118,7 +115,7 @@ when ODIN_OS == .Darwin || ODIN_OS == .FreeBSD || ODIN_OS == .Linux || ODIN_OS = return 0, true, stdout[0:index] } - foreign libc + foreign libc { popen :: proc(command: cstring, type: cstring) -> ^FILE --- pclose :: proc(stream: ^FILE) -> i32 --- @@ -127,7 +124,7 @@ when ODIN_OS == .Darwin || ODIN_OS == .FreeBSD || ODIN_OS == .Linux || ODIN_OS = } get_executable_path :: proc(allocator := context.temp_allocator) -> string { - exe_dir, err := os2.get_executable_directory(context.temp_allocator) + exe_dir, err := os.get_executable_directory(context.temp_allocator) if err != nil { log.error("Failed to resolve executable path: ", err) @@ -136,4 +133,3 @@ get_executable_path :: proc(allocator := context.temp_allocator) -> string { return exe_dir } - diff --git a/src/common/util_windows.odin b/src/common/util_windows.odin index 4281808..3be65b4 100644 --- a/src/common/util_windows.odin +++ b/src/common/util_windows.odin @@ -1,9 +1,7 @@ package common -import "core:fmt" import "core:log" import "core:mem" -import "core:strings" import "core:time" import win32 "core:sys/windows" diff --git a/src/main.odin b/src/main.odin index a77b2b4..c08f803 100644 --- a/src/main.odin +++ b/src/main.odin @@ -2,34 +2,26 @@ package main import "base:intrinsics" -import "core:encoding/json" import "core:fmt" import "core:log" import "core:mem" import "core:os" -import "core:reflect" -import "core:slice" -import "core:strconv" -import "core:strings" -import "core:sync" import "core:thread" -import "core:sys/windows" - import "src:common" import "src:server" VERSION := #config(VERSION, "dev") os_read :: proc(handle: rawptr, data: []byte) -> (int, int) { - ptr := cast(^os.Handle)handle - a, b := os.read(ptr^, data) + ptr := cast(^os.File)handle + a, b := os.read(ptr, data) return a, cast(int)(b != nil) } os_write :: proc(handle: rawptr, data: []byte) -> (int, int) { - ptr := cast(^os.Handle)handle - a, b := os.write(ptr^, data) + ptr := cast(^os.File)handle + a, b := os.write(ptr, data) return a, cast(int)(b != nil) } @@ -110,8 +102,8 @@ main :: proc() { fmt.println("ols version", VERSION) os.exit(0) } - reader := server.make_reader(os_read, cast(rawptr)&os.stdin) - writer := server.make_writer(os_write, cast(rawptr)&os.stdout) + reader := server.make_reader(os_read, cast(rawptr)os.stdin) + writer := server.make_writer(os_write, cast(rawptr)os.stdout) /* fh, err := os.open("log.txt", os.O_RDWR|os.O_CREATE) diff --git a/src/odin/format/format.odin b/src/odin/format/format.odin index e732d51..9b32721 100644 --- a/src/odin/format/format.odin +++ b/src/odin/format/format.odin @@ -18,8 +18,8 @@ find_config_file_or_default :: proc(path: string) -> printer.Config { //go up the directory until we find odinfmt.json path := path - ok: bool - if path, ok = filepath.abs(path); !ok { + err: os.Error + if path, err = filepath.abs(path, context.temp_allocator); err != nil { return default_style } @@ -27,14 +27,14 @@ find_config_file_or_default :: proc(path: string) -> printer.Config { found := false config := default_style - if (os.exists(name)) { - if data, ok := os.read_entire_file(name, context.temp_allocator); ok { + if os.exists(name) { + if data, err := os.read_entire_file(name, context.temp_allocator); err == nil { if json.unmarshal(data, &config) == nil { found = true } } } else { - new_path := filepath.join(elems = {path, ".."}, allocator = context.temp_allocator) + new_path, _ := filepath.join(elems = {path, ".."}, allocator = context.temp_allocator) //Currently the filepath implementation seems to stop at the root level, this might not be the best solution. if new_path == path { return default_style @@ -49,6 +49,26 @@ find_config_file_or_default :: proc(path: string) -> printer.Config { return config } +// Tries to read the config file from a given path instead +// of searching for it up a directory tree of a path +read_config_file_from_path_or_default :: proc(config_path: string) -> printer.Config { + path := config_path + err: os.Error + if path, err = filepath.abs(config_path, context.temp_allocator); err != nil { + return default_style + } + config := default_style + if os.exists(path) { + if data, err := os.read_entire_file(path, context.temp_allocator); err == nil { + if json.unmarshal(data, &config) == nil { + return config + } + } + } + + return default_style +} + format :: proc( filepath: string, source: string, diff --git a/src/odin/printer/printer.odin b/src/odin/printer/printer.odin index 227f909..61011ed 100644 --- a/src/odin/printer/printer.odin +++ b/src/odin/printer/printer.odin @@ -1,7 +1,5 @@ package odin_printer -import "core:fmt" -import "core:log" import "core:mem" import "core:odin/ast" import "core:odin/tokenizer" @@ -54,6 +52,8 @@ Config :: struct { inline_single_stmt_case: bool, spaces_around_colons: bool, //Put spaces to the left of a colon as well as the right. `foo: bar` => `foo : bar` space_single_line_blocks: bool, + align_struct_fields: bool, + align_struct_values: bool, } Brace_Style :: enum { @@ -90,7 +90,6 @@ Line_Suffix_Option :: enum { Indent, } - when ODIN_OS == .Windows { default_style := Config { spaces = 4, @@ -104,6 +103,8 @@ when ODIN_OS == .Windows { character_width = 100, sort_imports = true, spaces_around_colons = false, + align_struct_fields = true, + align_struct_values = true, } } else { default_style := Config { @@ -118,6 +119,8 @@ when ODIN_OS == .Windows { character_width = 100, sort_imports = true, spaces_around_colons = false, + align_struct_fields = true, + align_struct_values = true, } } diff --git a/src/odin/printer/visit.odin b/src/odin/printer/visit.odin index 6294dbe..b874dd7 100644 --- a/src/odin/printer/visit.odin +++ b/src/odin/printer/visit.odin @@ -1,6 +1,6 @@ +#+feature using-stmt package odin_printer -import "core:fmt" import "core:log" import "core:odin/ast" import "core:odin/parser" @@ -798,7 +798,7 @@ visit_comp_lit_exprs :: proc(p: ^Printer, comp_lit: ast.Comp_Lit, options := Lis alignment := get_possible_comp_lit_alignment(comp_lit.elems) if value, ok := expr.derived.(^ast.Field_Value); ok && alignment > 0 { align := empty() - if should_align_comp_lit(p, comp_lit) { + if should_align_comp_lit(p, comp_lit) && p.config.align_struct_values { align = repeat_space(alignment - get_node_length(value.field)) } document = cons( @@ -1007,10 +1007,10 @@ visit_stmt :: proc( } //Special case for when the if statement ends with a call expression - /* + /* if my_function( - - ) { + + ) { } */ if v.init != nil && is_value_decl_statement_ending_with_call(v.init) || @@ -2082,21 +2082,23 @@ visit_struct_field_list :: proc(p: ^Printer, list: ^ast.Field_List, options := L name_options := List_Options{.Add_Comma} - if (.Enforce_Newline in options) { - alignment := get_possible_field_alignment(list.list) - - if alignment > 0 { - length := 0 - for name in field.names { - length += get_node_length(name) + 2 - if .Using in field.flags { - length += 6 - } - if .Subtype in field.flags { - length += 9 + if (.Enforce_Newline in options) { + if p.config.align_struct_fields { + alignment := get_possible_field_alignment(list.list) + + if alignment > 0 { + length := 0 + for name in field.names { + length += get_node_length(name) + 2 + if .Using in field.flags { + length += 6 + } + if .Subtype in field.flags { + length += 9 + } } + align = repeat_space(alignment - length) } - align = repeat_space(alignment - length) } document = cons(document, visit_exprs(p, field.names, name_options)) } else { diff --git a/src/server/action.odin b/src/server/action.odin index d1608b5..11e8f86 100644 --- a/src/server/action.odin +++ b/src/server/action.odin @@ -71,6 +71,16 @@ get_code_actions :: proc(document: ^Document, range: common.Range, config: ^comm remove_unused_imports(document, strings.clone(document.uri.uri), config, &actions) } + if position_context.switch_stmt != nil || position_context.switch_type_stmt != nil { + add_populate_switch_cases_action( + &ast_context, + &position_context, + strings.clone(document.uri.uri), + &actions, + ) + } + add_invert_if_action(document, position_context.position, strings.clone(document.uri.uri), &actions) + return actions[:], true } diff --git a/src/server/action_invert_if_statements.odin b/src/server/action_invert_if_statements.odin new file mode 100644 index 0000000..047b3a2 --- /dev/null +++ b/src/server/action_invert_if_statements.odin @@ -0,0 +1,385 @@ +#+private file + +package server + +import "core:fmt" +import "core:log" +import "core:odin/ast" +import "core:odin/tokenizer" +import path "core:path/slashpath" +import "core:strings" + +import "src:common" + +/* + * The general idea behind inverting if statements is to allow + * if statements to be inverted without changing their behavior. + * The examples of these changes are provided in the tests. + * We should be careful to only allow this code action when it is safe to do so. + * So for now, we only support only one level of if statements without else-if chains. + */ + +@(private="package") +add_invert_if_action :: proc( + document: ^Document, + position: common.AbsolutePosition, + uri: string, + actions: ^[dynamic]CodeAction, +) { + if_stmt := find_if_stmt_at_position(document.ast.decls[:], position) + if if_stmt == nil { + return + } + + new_text, ok := generate_inverted_if(document, if_stmt) + if !ok { + return + } + + range := common.get_token_range(if_stmt^, document.ast.src) + + textEdits := make([dynamic]TextEdit, context.temp_allocator) + append(&textEdits, TextEdit{range = range, newText = new_text}) + + workspaceEdit: WorkspaceEdit + workspaceEdit.changes = make(map[string][]TextEdit, 0, context.temp_allocator) + workspaceEdit.changes[uri] = textEdits[:] + + append( + actions, + CodeAction { + kind = "refactor.more", + isPreferred = false, + title = "Invert if", + edit = workspaceEdit, + }, + ) +} + +// Find the innermost if statement that contains the given position +// This will NOT return else-if statements, only top-level if statements +// Also will not return an if statement if the position is in its else clause +find_if_stmt_at_position :: proc(stmts: []^ast.Stmt, position: common.AbsolutePosition) -> ^ast.If_Stmt { + for stmt in stmts { + if stmt == nil { + continue + } + if result := find_if_stmt_in_node(stmt, position, false); result != nil { + return result + } + } + return nil +} + +find_if_stmt_in_node :: proc(node: ^ast.Node, position: common.AbsolutePosition, in_else_clause: bool) -> ^ast.If_Stmt { + if node == nil { + return nil + } + + if !(node.pos.offset <= position && position <= node.end.offset) { + return nil + } + + #partial switch n in node.derived { + case ^ast.If_Stmt: + // First check if position is in the else clause + if n.else_stmt != nil && position_in_node(n.else_stmt, position) { + // Position is in the else clause - look for nested ifs inside it + // but mark that we're in an else clause + if nested := find_if_stmt_in_node(n.else_stmt, position, true); nested != nil { + return nested + } + // Position is in else clause but not on a valid nested if + // Don't return the current if statement + return nil + } + + if n.body != nil && position_in_node(n.body, position) { + if nested := find_if_stmt_in_node(n.body, position, false); nested != nil { + return nested + } + // Position is inside the body but no nested if found + // Don't return the current if statement + return nil + } + + // Position is in the condition/init part or we're the closest if + // Only return this if statement if we're NOT in an else clause + // (i.e., this is not an else-if) + if !in_else_clause { + return n + } + return nil + + case ^ast.Block_Stmt: + for stmt in n.stmts { + if result := find_if_stmt_in_node(stmt, position, false); result != nil { + return result + } + } + + case ^ast.Proc_Lit: + if n.body != nil { + return find_if_stmt_in_node(n.body, position, false) + } + + case ^ast.Value_Decl: + for value in n.values { + if result := find_if_stmt_in_node(value, position, false); result != nil { + return result + } + } + + case ^ast.For_Stmt: + if n.body != nil { + return find_if_stmt_in_node(n.body, position, false) + } + + case ^ast.Range_Stmt: + if n.body != nil { + return find_if_stmt_in_node(n.body, position, false) + } + + case ^ast.Switch_Stmt: + if n.body != nil { + return find_if_stmt_in_node(n.body, position, false) + } + + case ^ast.Type_Switch_Stmt: + if n.body != nil { + return find_if_stmt_in_node(n.body, position, false) + } + + case ^ast.Case_Clause: + for stmt in n.body { + if result := find_if_stmt_in_node(stmt, position, false); result != nil { + return result + } + } + + case ^ast.When_Stmt: + if n.body != nil { + if result := find_if_stmt_in_node(n.body, position, false); result != nil { + return result + } + } + if n.else_stmt != nil { + if result := find_if_stmt_in_node(n.else_stmt, position, false); result != nil { + return result + } + } + + case ^ast.Defer_Stmt: + if n.stmt != nil { + return find_if_stmt_in_node(n.stmt, position, false) + } + } + + return nil +} + +// Generate the inverted if statement text +generate_inverted_if :: proc(document: ^Document, if_stmt: ^ast.If_Stmt) -> (string, bool) { + src := document.ast.src + + indent := get_line_indentation(src, if_stmt.pos.offset) + + sb := strings.builder_make(context.temp_allocator) + + if if_stmt.label != nil { + label_text := src[if_stmt.label.pos.offset:if_stmt.label.end.offset] + strings.write_string(&sb, label_text) + strings.write_string(&sb, ": ") + } + + strings.write_string(&sb, "if ") + + if if_stmt.init != nil { + init_text := src[if_stmt.init.pos.offset:if_stmt.init.end.offset] + strings.write_string(&sb, init_text) + strings.write_string(&sb, "; ") + } + + if if_stmt.cond != nil { + inverted_cond, ok := invert_condition(src, if_stmt.cond) + if !ok { + return "", false + } + strings.write_string(&sb, inverted_cond) + } + + strings.write_string(&sb, " ") + + // Now we need to swap the bodies + + if if_stmt.else_stmt != nil { + else_body_text := get_block_body_text(src, if_stmt.else_stmt, indent) + then_body_text := get_block_body_text(src, if_stmt.body, indent) + + strings.write_string(&sb, "{\n") + strings.write_string(&sb, else_body_text) + strings.write_string(&sb, indent) + strings.write_string(&sb, "} else {\n") + strings.write_string(&sb, then_body_text) + strings.write_string(&sb, indent) + strings.write_string(&sb, "}") + } else { + then_body_text := get_block_body_text(src, if_stmt.body, indent) + + strings.write_string(&sb, "{\n") + strings.write_string(&sb, indent) + strings.write_string(&sb, "} else {\n") + strings.write_string(&sb, then_body_text) + strings.write_string(&sb, indent) + strings.write_string(&sb, "}") + } + + return strings.to_string(sb), true +} + +// Get the indentation (leading whitespace) of the line containing the given offset +@(private="package") +get_line_indentation :: proc(src: string, offset: int) -> string { + line_start := offset + for line_start > 0 && src[line_start - 1] != '\n' { + line_start -= 1 + } + + indent_end := line_start + for indent_end < len(src) && (src[indent_end] == ' ' || src[indent_end] == '\t') { + indent_end += 1 + } + + return src[line_start:indent_end] +} + +// Extract the body text from a block statement (without the braces) +get_block_body_text :: proc(src: string, stmt: ^ast.Stmt, base_indent: string) -> string { + if stmt == nil { + return "" + } + + #partial switch block in stmt.derived { + case ^ast.Block_Stmt: + if len(block.stmts) == 0 { + return "" + } + + sb := strings.builder_make(context.temp_allocator) + + for s in block.stmts { + if s == nil { + continue + } + stmt_indent := get_line_indentation(src, s.pos.offset) + stmt_text := src[s.pos.offset:s.end.offset] + strings.write_string(&sb, stmt_indent) + strings.write_string(&sb, stmt_text) + strings.write_string(&sb, "\n") + } + + return strings.to_string(sb) + + case ^ast.If_Stmt: + // This is an else-if, need to handle it recursively + if_text, ok := generate_inverted_if_for_else(src, block, base_indent) + if ok { + return if_text + } + } + + // Fallback: just return the statement text + stmt_text := src[stmt.pos.offset:stmt.end.offset] + return fmt.tprintf("%s%s\n", base_indent, stmt_text) +} + +// For else-if chains, we don't invert them, just preserve +generate_inverted_if_for_else :: proc(src: string, if_stmt: ^ast.If_Stmt, base_indent: string) -> (string, bool) { + stmt_indent := get_line_indentation(src, if_stmt.pos.offset) + stmt_text := src[if_stmt.pos.offset:if_stmt.end.offset] + return fmt.tprintf("%s%s\n", stmt_indent, stmt_text), true +} + +// Invert a condition expression +invert_condition :: proc(src: string, cond: ^ast.Expr) -> (string, bool) { + if cond == nil { + return "", false + } + + #partial switch c in cond.derived { + case ^ast.Binary_Expr: + inverted_op, can_invert := get_inverted_operator(c.op.kind) + if can_invert { + left_text := src[c.left.pos.offset:c.left.end.offset] + right_text := src[c.right.pos.offset:c.right.end.offset] + return fmt.tprintf("%s %s %s", left_text, inverted_op, right_text), true + } + + if c.op.kind == .Cmp_And || c.op.kind == .Cmp_Or { + // Just wrap with !() + cond_text := src[cond.pos.offset:cond.end.offset] + return fmt.tprintf("!(%s)", cond_text), true + } + + case ^ast.Unary_Expr: + // If it's already negated with !, remove the negation + if c.op.kind == .Not { + inner_text := src[c.expr.pos.offset:c.expr.end.offset] + return inner_text, true + } + + case ^ast.Paren_Expr: + inner_inverted, ok := invert_condition(src, c.expr) + if ok { + if needs_parentheses(inner_inverted) { + return fmt.tprintf("(%s)", inner_inverted), true + } + return inner_inverted, true + } + } + + // Default: wrap the whole condition with !() + cond_text := src[cond.pos.offset:cond.end.offset] + if is_simple_expr(cond) { + return fmt.tprintf("!%s", cond_text), true + } + return fmt.tprintf("!(%s)", cond_text), true +} + +// Check if an expression is simple (identifier, call, or already parenthesized) +is_simple_expr :: proc(expr: ^ast.Expr) -> bool { + if expr == nil { + return false + } + #partial switch e in expr.derived { + case ^ast.Ident, ^ast.Paren_Expr, ^ast.Call_Expr, ^ast.Selector_Expr, ^ast.Index_Expr: + return true + } + return false +} + +// Check if a string needs parentheses (simple heuristic) +needs_parentheses :: proc(s: string) -> bool { + // If it starts with ! and is not wrapped in parens, it might need them + // This is a simple heuristic + return strings.contains(s, " && ") || strings.contains(s, " || ") +} + +// Get the inverted comparison operator +get_inverted_operator :: proc(op: tokenizer.Token_Kind) -> (string, bool) { + #partial switch op { + case .Cmp_Eq: + return "!=", true + case .Not_Eq: + return "==", true + case .Lt: + return ">=", true + case .Lt_Eq: + return ">", true + case .Gt: + return "<=", true + case .Gt_Eq: + return "<", true + } + return "", false +} diff --git a/src/server/action_populate_switch_cases.odin b/src/server/action_populate_switch_cases.odin new file mode 100644 index 0000000..1e60f15 --- /dev/null +++ b/src/server/action_populate_switch_cases.odin @@ -0,0 +1,211 @@ +#+private file + +package server + +import "core:fmt" +import "core:odin/ast" +import "core:odin/tokenizer" +import "core:strings" + +import "src:common" + + +// Get the offset of the start of the line containing the given offset +get_line_start_offset :: proc(src: string, offset: int) -> int { + line_start := offset + for line_start > 0 && src[line_start - 1] != '\n' { + line_start -= 1 + } + return line_start +} + +get_block_original_text :: proc(block: []^ast.Stmt, document_text: string) -> string { + if len(block) == 0 { + return "" + } + start := get_line_start_offset(document_text, block[0].pos.offset) + end := block[max(0, len(block) - 1)].end.offset + return string(document_text[start:end]) +} + +SwitchBlockInfo :: struct { + names: []string, + existing_cases: map[string]struct{}, + switch_indentation: string, + is_enum: bool, + pos: tokenizer.Pos, +} + +get_switch_cases_info :: proc( + ast_context: ^AstContext, + position_context: ^DocumentPositionContext, +) -> ( + SwitchBlockInfo, + bool, +) { + if position_context.switch_stmt == nil && position_context.switch_type_stmt == nil { + return {}, false + } + + if position_context.switch_stmt != nil && position_context.switch_stmt.cond == nil { + return {}, false + } + + switch_block: ^ast.Block_Stmt + found_switch_block: bool + is_enum: bool + pos: tokenizer.Pos + if position_context.switch_stmt != nil { + switch_block, found_switch_block = position_context.switch_stmt.body.derived.(^ast.Block_Stmt) + is_enum = true + pos = position_context.switch_stmt.pos + } + + if !found_switch_block && position_context.switch_type_stmt != nil { + switch_block, found_switch_block = position_context.switch_type_stmt.body.derived.(^ast.Block_Stmt) + pos = position_context.switch_type_stmt.pos + } + + if !found_switch_block { + return {}, false + } + switch_indentation := get_line_indentation(ast_context.file.src, switch_block.pos.offset) + existing_cases := make(map[string]struct{}, context.temp_allocator) + + + for stmt in switch_block.stmts { + if case_clause, ok := stmt.derived.(^ast.Case_Clause); ok { + for clause in case_clause.list { + if is_enum { + if name, ok := get_used_switch_name(clause); ok && name != "" { + existing_cases[name] = {} + } + } else { + reset_ast_context(ast_context) + if symbol, ok := resolve_type_expression(ast_context, clause); ok { + name := get_qualified_union_case_name(&symbol, ast_context, position_context) + //TODO: this is wrong for anonymous enums and structs, where the name field is "enum" or "struct" respectively but we want to use the full signature + //we also can't use the signature all the time because type aliases need to use specifically the alias name here and not the signature + if name == "" { + name = get_signature(ast_context, symbol) + } + if name != "" { + existing_cases[name] = {} + } + } + } + } + pos = case_clause.stmt_base.end + } + } + if is_enum { + enum_value, was_super_enum, unwrap_ok := unwrap_enum(ast_context, position_context.switch_stmt.cond) + if !unwrap_ok { + return {}, false + } + return SwitchBlockInfo { + names = enum_value.names, + existing_cases = existing_cases, + switch_indentation = switch_indentation, + is_enum = !was_super_enum, + pos = pos, + }, + true + } + + st := position_context.switch_type_stmt + if st == nil { + return {}, false + } + reset_ast_context(ast_context) + union_value, unwrap_ok := unwrap_union(ast_context, st.tag.derived.(^ast.Assign_Stmt).rhs[0]) + if !unwrap_ok { + return {}, false + } + all_case_names := make([]string, len(union_value.types), context.temp_allocator) + for t, i in union_value.types { + reset_ast_context(ast_context) + if symbol, ok := resolve_type_expression(ast_context, t); ok { + case_name := get_qualified_union_case_name(&symbol, ast_context, position_context) + //TODO: this is wrong for anonymous enums and structs, where the name field is "enum" or "struct" respectively but we want to use the full signature + //we also can't use the signature all the time because type aliases need to use specifically the alias name here and not the signature + if case_name == "" { + case_name = get_signature(ast_context, symbol) + } + all_case_names[i] = case_name + } else { + all_case_names[i] = "invalid type expression" + } + } + return SwitchBlockInfo { + names = all_case_names, + existing_cases = existing_cases, + switch_indentation = switch_indentation, + is_enum = false, + pos = pos, + }, + true +} + +create_populate_switch_cases_edit :: proc( + position_context: ^DocumentPositionContext, + info: SwitchBlockInfo, +) -> ( + TextEdit, + bool, +) { + //we need to be either in a switch stmt or a switch type stmt + if position_context.switch_stmt == nil && position_context.switch_type_stmt == nil { + return {}, false + } + + pos := info.pos + pos.line += 1 + pos.column = 1 + + position := common.token_pos_to_position(pos, position_context.file.src) + + range := common.Range { + start = position, + end = position, + } + + replacement_builder := strings.builder_make() + dot := info.is_enum ? "." : "" + b := &replacement_builder + for name in info.names { + if name in info.existing_cases {continue} + fmt.sbprintln(b, info.switch_indentation, "case ", dot, name, ":", sep = "") + } + return TextEdit{range = range, newText = strings.to_string(replacement_builder)}, true +} + +@(private = "package") +add_populate_switch_cases_action :: proc( + ast_context: ^AstContext, + position_context: ^DocumentPositionContext, + uri: string, + actions: ^[dynamic]CodeAction, +) { + info, ok := get_switch_cases_info(ast_context, position_context) + if !ok {return} + + if len(info.existing_cases) == len(info.names) {return} //action not needed + edit, edit_ok := create_populate_switch_cases_edit(position_context, info) + if !edit_ok {return} + textEdits := make([dynamic]TextEdit, context.temp_allocator) + append(&textEdits, edit) + + workspaceEdit: WorkspaceEdit + workspaceEdit.changes = make(map[string][]TextEdit, 0, context.temp_allocator) + workspaceEdit.changes[uri] = textEdits[:] + append( + actions, + CodeAction { + kind = "refactor.rewrite", + isPreferred = true, + title = "populate remaining switch cases", + edit = workspaceEdit, + }, + ) +} diff --git a/src/server/analysis.odin b/src/server/analysis.odin index a6f3a64..1a85708 100644 --- a/src/server/analysis.odin +++ b/src/server/analysis.odin @@ -1,4 +1,5 @@ #+feature dynamic-literals +#+feature using-stmt package server import "core:fmt" @@ -437,7 +438,7 @@ is_symbol_same_typed :: proc(ast_context: ^AstContext, a, b: Symbol, flags: ast. case SymbolBasicValue: b_value := b.value.(SymbolBasicValue) return a_value.ident.name == b_value.ident.name && a.pkg == b.pkg - case SymbolStructValue, SymbolEnumValue, SymbolUnionValue, SymbolBitSetValue: + case SymbolStructValue, SymbolEnumValue, SymbolUnionValue, SymbolBitSetValue, SymbolBitFieldValue: return a.name == b.name && a.pkg == b.pkg case SymbolSliceValue: b_value := b.value.(SymbolSliceValue) @@ -1364,12 +1365,69 @@ resolve_call_expr :: proc(ast_context: ^AstContext, v: ^ast.Call_Expr) -> (Symbo } else { return {}, false } + } else if directive, ok := v.expr.derived.(^ast.Basic_Directive); ok { + return resolve_call_directive(ast_context, v) } ok := internal_resolve_type_expression(ast_context, v.expr, &symbol) return symbol, ok } +resolve_call_directive :: proc(ast_context: ^AstContext, call: ^ast.Call_Expr) -> (Symbol, bool) { + directive, ok := call.expr.derived.(^ast.Basic_Directive) + if !ok { + return {}, false + } + + switch directive.name { + case "config": + if len(call.args) > 1 { + return resolve_type_expression(ast_context, call.args[1]) + } + case "load": + if len(call.args) == 1 { + ident := new_type(ast.Ident, call.pos, call.end, ast_context.allocator) + ident.name = "u8" + value := SymbolSliceValue { + expr = ident, + } + symbol := Symbol { + name = "#load", + pkg = ast_context.current_package, + value = value, + } + return symbol, true + } else if len(call.args) == 2 { + return resolve_type_expression(ast_context, call.args[1]) + } + case "location": + return lookup("Source_Code_Location", indexer.runtime_package, call.pos.file) + case "hash", "load_hash": + ident := new_type(ast.Ident, call.pos, call.end, ast_context.allocator) + ident.name = "int" + return resolve_type_identifier(ast_context, ident^) + case "load_directory": + pkg := new_type(ast.Ident, call.pos, call.end, ast_context.allocator) + pkg.name = "runtime" + field := new_type(ast.Ident, call.pos, call.end, ast_context.allocator) + field.name = "Load_Directory_File" + selector := new_type(ast.Selector_Expr, call.pos, call.end, ast_context.allocator) + selector.expr = pkg + selector.field = field + value := SymbolSliceValue { + expr = selector, + } + symbol := Symbol { + name = "#load_directory", + pkg = ast_context.current_package, + value = value, + } + return symbol, true + } + + return {}, false +} + resolve_index_expr :: proc(ast_context: ^AstContext, index_expr: ^ast.Index_Expr, expr: ^ast.Expr) -> (Symbol, bool) { indexed := Symbol{} ok := internal_resolve_type_expression(ast_context, expr, &indexed) @@ -1382,11 +1440,23 @@ resolve_index_expr :: proc(ast_context: ^AstContext, index_expr: ^ast.Index_Expr #partial switch v in indexed.value { case SymbolDynamicArrayValue: + if .Soa in indexed.flags { + indexed.flags |= { .SoaPointer } + return indexed, true + } ok = internal_resolve_type_expression(ast_context, v.expr, &symbol) case SymbolSliceValue: ok = internal_resolve_type_expression(ast_context, v.expr, &symbol) + if .Soa in indexed.flags { + indexed.flags |= { .SoaPointer } + return indexed, true + } case SymbolFixedArrayValue: ok = internal_resolve_type_expression(ast_context, v.expr, &symbol) + if .Soa in indexed.flags { + indexed.flags |= { .SoaPointer } + return indexed, true + } case SymbolMapValue: ok = internal_resolve_type_expression(ast_context, v.value, &symbol) case SymbolMultiPointerValue: @@ -1424,6 +1494,9 @@ resolve_index_expr :: proc(ast_context: ^AstContext, index_expr: ^ast.Index_Expr } symbol.type = indexed.type + if .Soa in indexed.flags { + symbol.flags |= {.SoaPointer} + } return symbol, ok } @@ -1741,7 +1814,7 @@ internal_resolve_type_identifier :: proc(ast_context: ^AstContext, node: ast.Ide try_build_package(symbol.pkg) - return symbol, true + return resolve_symbol_return(ast_context, symbol) } } @@ -1752,8 +1825,9 @@ internal_resolve_type_identifier :: proc(ast_context: ^AstContext, node: ast.Ide pkg = indexer.runtime_package, value = SymbolPackageValue{}, } + try_build_package(symbol.pkg) - return symbol, true + return resolve_symbol_return(ast_context, symbol) } if global, ok := ast_context.globals[node.name]; @@ -1782,7 +1856,7 @@ internal_resolve_type_identifier :: proc(ast_context: ^AstContext, node: ast.Ide try_build_package(symbol.pkg) - return symbol, true + return resolve_symbol_return(ast_context, symbol) } is_runtime := strings.contains(ast_context.current_package, "base/runtime") @@ -1825,7 +1899,7 @@ resolve_local_identifier :: proc(ast_context: ^AstContext, node: ast.Ident, loca value = SymbolPackageValue{}, } - return symbol, true + return resolve_symbol_return(ast_context, symbol) } } } @@ -1907,12 +1981,15 @@ resolve_local_identifier :: proc(ast_context: ^AstContext, node: ast.Ident, loca if .Variable in local.flags { return_symbol.flags |= {.Variable} } + if .PolyType in local.flags { + return_symbol.flags |= {.PolyType} + } return_symbol.flags |= {.Local} return_symbol.value_expr = local.value_expr return_symbol.type_expr = local.type_expr - return_symbol.doc = get_doc(local.docs, ast_context.allocator) - return_symbol.comment = get_comment(local.comment) + return_symbol.doc = get_comment(local.docs, ast_context.allocator) + return_symbol.comment = get_comment(local.comment, ast_context.allocator) return return_symbol, ok } @@ -1941,8 +2018,9 @@ resolve_global_identifier :: proc(ast_context: ^AstContext, node: ast.Ident, glo defer { ast_context.call = old_call } - - if ok = internal_resolve_type_expression(ast_context, v.expr, &return_symbol); ok { + if _, ok = v.expr.derived.(^ast.Basic_Directive); ok { + return_symbol, ok = resolve_call_directive(ast_context, v) + } else if ok = internal_resolve_type_expression(ast_context, v.expr, &return_symbol); ok { return_types := get_proc_return_types(ast_context, return_symbol, v, .Mutable in global.flags) if len(return_types) > 0 { ok = internal_resolve_type_expression(ast_context, return_types[0], &return_symbol) @@ -2000,11 +2078,11 @@ resolve_global_identifier :: proc(ast_context: ^AstContext, node: ast.Ident, glo } if global.docs != nil { - return_symbol.doc = get_doc(global.docs, ast_context.allocator) + return_symbol.doc = get_comment(global.docs, ast_context.allocator) } if global.comment != nil { - return_symbol.comment = get_comment(global.comment) + return_symbol.comment = get_comment(global.comment, ast_context.allocator) } return_symbol.type_expr = global.type_expr @@ -2225,6 +2303,9 @@ internal_resolve_comp_literal :: proc( set_ast_package_set_scoped(ast_context, symbol.pkg) + if position_context.parent_comp_lit == nil { + return {}, false + } symbol, _ = resolve_type_comp_literal( ast_context, position_context, @@ -2403,6 +2484,11 @@ resolve_implicit_selector :: proc( if position_context.call != nil { if call, ok := position_context.call.derived.(^ast.Call_Expr); ok { parameter_index, parameter_ok := find_position_in_call_param(position_context, call^) + old := ast_context.resolve_specific_overload + ast_context.resolve_specific_overload = true + defer { + ast_context.resolve_specific_overload = old + } if symbol, ok := resolve_type_expression(ast_context, call.expr); ok && parameter_ok { if proc_value, ok := symbol.value.(SymbolProcedureValue); ok { if len(proc_value.arg_types) <= parameter_index { @@ -2556,6 +2642,16 @@ resolve_symbol_return :: proc(ast_context: ^AstContext, symbol: Symbol, ok := tr } #partial switch &v in symbol.value { + case SymbolPackageValue: + if pkg, ok := indexer.index.collection.packages[symbol.pkg]; ok { + if symbol.doc == "" { + symbol.doc = strings.to_string(pkg.doc) + } + if symbol.comment == "" { + symbol.comment = strings.to_string(pkg.comment) + } + } + return symbol, true case SymbolProcedureGroupValue: if s, ok := resolve_function_overload(ast_context, v.group.derived.(^ast.Proc_Group)); ok { if s.doc == "" { @@ -3313,7 +3409,7 @@ get_package_from_node :: proc(node: ast.Node) -> string { } get_package_from_filepath :: proc(file_path: string) -> string { - slashed, _ := filepath.to_slash(file_path, context.temp_allocator) + slashed, _ := filepath.replace_path_separators(file_path, '/', context.temp_allocator) ret := path.dir(slashed, context.temp_allocator) return ret } diff --git a/src/server/ast.odin b/src/server/ast.odin index 59e3de2..b872e11 100644 --- a/src/server/ast.odin +++ b/src/server/ast.odin @@ -1,4 +1,5 @@ #+feature dynamic-literals +#+feature using-stmt package server import "core:fmt" @@ -7,6 +8,7 @@ import "core:odin/ast" import "core:odin/parser" import path "core:path/slashpath" import "core:strings" +import "core:log" keyword_map: map[string]struct{} = { "typeid" = {}, @@ -374,7 +376,7 @@ merge_attributes :: proc(attrs: []^ast.Attribute, foreign_attrs: []^ast.Attribut // a const variable declaration, so we do a quick check here to distinguish the cases. is_variable_declaration :: proc(expr: ^ast.Expr) -> bool { #partial switch v in expr.derived { - case ^ast.Comp_Lit, ^ast.Basic_Lit, ^ast.Type_Cast, ^ast.Call_Expr, ^ast.Binary_Expr: + case ^ast.Comp_Lit, ^ast.Basic_Lit, ^ast.Type_Cast, ^ast.Call_Expr, ^ast.Binary_Expr, ^ast.Unary_Expr: return true case: return false @@ -446,9 +448,9 @@ collect_value_decl :: proc( global_expr.name_expr = name if len(value_decl.values) > i { + global_expr.value_expr = value_decl.values[i] if is_variable_declaration(value_decl.values[i]) { global_expr.flags += {.Variable} - global_expr.value_expr = value_decl.values[i] } } if value_decl.type != nil { @@ -560,54 +562,82 @@ collect_globals :: proc(file: ast.File) -> []GlobalExpr { } get_ast_node_string :: proc(node: ^ast.Node, src: string) -> string { - return string(src[node.pos.offset:node.end.offset]) + return strings.trim_prefix(string(src[node.pos.offset:node.end.offset]), "$") } -get_doc :: proc(comment: ^ast.Comment_Group, allocator: mem.Allocator) -> string { - if comment == nil { - return "" +COMMENT_DELIMITER_LENGTH :: len("//") +#assert(COMMENT_DELIMITER_LENGTH == len("/*")) +#assert(COMMENT_DELIMITER_LENGTH == len("*/")) + +// Returns the minimum indentation across all non-empty lines +get_min_indent :: proc(lines: []string) -> int { + min_indent := max(int) + for line in lines { + if strings.trim_space(line) == "" do continue + for c, i in line { + if !strings.is_space(c) { + min_indent = min(min_indent, i) + break + } + } } + return 0 if min_indent == max(int) else min_indent +} - tmp: string - - for doc in comment.list { - if strings.starts_with(doc.text, "/*") && doc.pos.column != 1 { - lines := strings.split(doc.text, "\n", context.temp_allocator) - for line, i in lines { - if i != 0 && len(line) > 0 { - column := 0 - for column < doc.pos.column - 1 { - if line[column] == '\t' || line[column] == ' ' { - column += 1 - } else { - break - } - } - tmp = strings.concatenate({tmp, "\n", line[column:]}, context.temp_allocator) - } else { - tmp = strings.concatenate({tmp, "\n", line}, context.temp_allocator) - } - } +// Strips min_indent characters from each line and joins with newlines +strip_indent_and_join :: proc(lines: []string, min_indent: int, allocator: mem.Allocator) -> string { + result := make([dynamic]string, context.temp_allocator) + for line in lines { + if len(line) >= min_indent { + append(&result, line[min_indent:]) } else { - tmp = strings.concatenate({tmp, "\n", doc.text}, context.temp_allocator) + append(&result, strings.trim_left_space(line)) } } + return strings.join(result[:], "\n", allocator) +} - if tmp != "" { - no_lines, _ := strings.replace_all(tmp, "//", "", context.temp_allocator) - no_begin_comments, _ := strings.replace_all(no_lines, "/*", "", context.temp_allocator) - no_end_comments, _ := strings.replace_all(no_begin_comments, "*/", "", context.temp_allocator) - return strings.clone(no_end_comments, allocator) - } +// Aggregates the content from the provided comment group, +// omitting extraneous spaces and delimiters. +get_comment :: proc(comment: ^ast.Comment_Group, allocator := context.allocator) -> string { + if comment == nil do return "" - return "" -} + lines := make([dynamic]string, context.temp_allocator) + + for token in comment.list { + if len(token.text) < COMMENT_DELIMITER_LENGTH do continue + delimiter := token.text[:COMMENT_DELIMITER_LENGTH] + + switch delimiter { + case "/*": + if len(token.text) <= COMMENT_DELIMITER_LENGTH * 2 do continue + content := token.text[COMMENT_DELIMITER_LENGTH:len(token.text) - COMMENT_DELIMITER_LENGTH] -get_comment :: proc(comment: ^ast.Comment_Group) -> string { - if comment != nil && len(comment.list) > 0 { - return comment.list[0].text + // Check if this is a single-line block comment (no newlines) + if !strings.contains(content, "\n") { + text := strings.trim_space(content) + if text != "" do append(&lines, text) + } else { + // Multi-line block comment: strip leading/trailing newlines + content = strings.trim(content, "\r\n") + for line in strings.split_lines(content, context.temp_allocator) { + append(&lines, line) + } + } + + case "//": + text := token.text[COMMENT_DELIMITER_LENGTH:] + append(&lines, text) + + case: + log.error("unsupported comment delimiter") + } } - return "" + + if len(lines) == 0 do return "" + + min_indent := get_min_indent(lines[:]) + return strip_indent_and_join(lines[:], min_indent, allocator) } free_ast :: proc { diff --git a/src/server/build.odin b/src/server/build.odin index 59dc63d..6df21a8 100644 --- a/src/server/build.odin +++ b/src/server/build.odin @@ -2,6 +2,7 @@ package server import "base:runtime" +import "core:slice" import "core:fmt" import "core:log" @@ -126,6 +127,25 @@ skip_file :: proc(filename: string) -> bool { return false } +// Finds all packages under the provided path by walking the file system +// and appends them to the provided dynamic array +append_packages :: proc( + path: string, + pkgs: ^[dynamic]string, + allocator := context.temp_allocator, +) { + w := os.walker_create(path) + defer os.walker_destroy(&w) + for info in os.walker_walk(&w) { + if info.type != .Directory && filepath.ext(info.name) == ".odin" { + dir := filepath.dir(info.fullpath, allocator) + if !slice.contains(pkgs[:], dir) { + append(pkgs, dir) + } + } + } +} + should_collect_file :: proc(file_tags: parser.File_Tags) -> bool { if file_tags.ignore { return false @@ -165,8 +185,8 @@ try_build_package :: proc(pkg_name: string) { matches, err := filepath.glob(fmt.tprintf("%v/*.odin", pkg_name), context.temp_allocator) - if err != .None { - log.errorf("Failed to glob %v for indexing package", pkg_name) + if err != nil && err != .Not_Exist { + log.errorf("Failed to glob %v for indexing package: %v", pkg_name, err) return } @@ -182,10 +202,10 @@ try_build_package :: proc(pkg_name: string) { continue } - data, ok := os.read_entire_file(fullpath, context.allocator) + data, err := os.read_entire_file(fullpath, context.allocator) - if !ok { - log.errorf("failed to read entire file for indexing %v", fullpath) + if err != nil { + log.errorf("failed to read entire file for indexing %v: %v", fullpath, err) continue } @@ -212,10 +232,11 @@ try_build_package :: proc(pkg_name: string) { pkg = pkg, } - ok = parser.parse_file(&p, &file) + ok := parser.parse_file(&p, &file) if !ok { - if !strings.contains(fullpath, "builtin.odin") && !strings.contains(fullpath, "intrinsics.odin") { + if !strings.contains(fullpath, "builtin.odin") && + !strings.contains(fullpath, "intrinsics.odin") { log.errorf("error in parse file for indexing %v", fullpath) } continue @@ -229,9 +250,10 @@ try_build_package :: proc(pkg_name: string) { } } - build_cache.loaded_pkgs[strings.clone(pkg_name, indexer.index.collection.allocator)] = PackageCacheInfo { - timestamp = time.now(), - } + build_cache.loaded_pkgs[strings.clone(pkg_name, indexer.index.collection.allocator)] = + PackageCacheInfo { + timestamp = time.now(), + } } @@ -241,7 +263,7 @@ remove_index_file :: proc(uri: common.Uri) -> common.Error { fullpath := uri.path when ODIN_OS == .Windows { - fullpath, _ = filepath.to_slash(fullpath, context.temp_allocator) + fullpath, _ = filepath.replace_path_separators(fullpath, '/', context.temp_allocator) } corrected_uri := common.create_uri(fullpath, context.temp_allocator) @@ -273,14 +295,14 @@ index_file :: proc(uri: common.Uri, text: string) -> common.Error { fullpath := uri.path p := parser.Parser { - err = log_error_handler, - warn = log_warning_handler, - flags = {.Optional_Semicolons}, - } + err = log_error_handler, + warn = log_warning_handler, + flags = {.Optional_Semicolons}, + } when ODIN_OS == .Windows { correct := common.get_case_sensitive_path(fullpath, context.temp_allocator) - fullpath, _ = filepath.to_slash(correct, context.temp_allocator) + fullpath, _ = filepath.replace_path_separators(correct, '/', context.temp_allocator) } dir := filepath.base(filepath.dir(fullpath, context.temp_allocator)) @@ -295,10 +317,10 @@ index_file :: proc(uri: common.Uri, text: string) -> common.Error { } file := ast.File { - fullpath = fullpath, - src = text, - pkg = pkg, - } + fullpath = fullpath, + src = text, + pkg = pkg, + } { allocator := context.allocator @@ -308,7 +330,8 @@ index_file :: proc(uri: common.Uri, text: string) -> common.Error { ok = parser.parse_file(&p, &file) if !ok { - if !strings.contains(fullpath, "builtin.odin") && !strings.contains(fullpath, "intrinsics.odin") { + if !strings.contains(fullpath, "builtin.odin") && + !strings.contains(fullpath, "intrinsics.odin") { log.errorf("error in parse file for indexing %v", fullpath) } } diff --git a/src/server/caches.odin b/src/server/caches.odin index ff7a422..519ce6c 100644 --- a/src/server/caches.odin +++ b/src/server/caches.odin @@ -76,26 +76,13 @@ clear_all_package_aliases :: proc() { //Go through all the collections to find all the possible packages that exists find_all_package_aliases :: proc() { - walk_proc :: proc(info: os.File_Info, in_err: os.Errno, user_data: rawptr) -> (err: os.Errno, skip_dir: bool) { - data := cast(^[dynamic]string)user_data - - if !info.is_dir && filepath.ext(info.name) == ".odin" { - dir := filepath.dir(info.fullpath, context.temp_allocator) - if !slice.contains(data[:], dir) { - append(data, dir) - } - } - - return in_err, false - } - for k, v in common.config.collections { pkgs := make([dynamic]string, context.temp_allocator) - filepath.walk(v, walk_proc, &pkgs) + append_packages(v, &pkgs, context.temp_allocator) for pkg in pkgs { if pkg, err := filepath.rel(v, pkg, context.temp_allocator); err == .None { - forward_pkg, _ := filepath.to_slash(pkg, context.temp_allocator) + forward_pkg, _ := filepath.replace_path_separators(pkg, '/', context.temp_allocator) if k not_in build_cache.pkg_aliases { build_cache.pkg_aliases[k] = make([dynamic]string) } diff --git a/src/server/check.odin b/src/server/check.odin index fed3a3f..1280b76 100644 --- a/src/server/check.odin +++ b/src/server/check.odin @@ -1,6 +1,5 @@ package server -import "base:intrinsics" import "base:runtime" import "core:encoding/json" @@ -11,11 +10,7 @@ import "core:os" import "core:path/filepath" import path "core:path/slashpath" import "core:slice" -import "core:strconv" import "core:strings" -import "core:sync" -import "core:text/scanner" -import "core:thread" import "src:common" @@ -41,24 +36,11 @@ Json_Errors :: struct { //If the user does not specify where to call odin check, it'll just find all directory with odin, and call them seperately. fallback_find_odin_directories :: proc(config: ^common.Config) -> []string { - walk_proc :: proc(info: os.File_Info, in_err: os.Errno, user_data: rawptr) -> (err: os.Errno, skip_dir: bool) { - data := cast(^[dynamic]string)user_data - - if !info.is_dir && filepath.ext(info.name) == ".odin" { - dir := filepath.dir(info.fullpath, context.temp_allocator) - if !slice.contains(data[:], dir) { - append(data, dir) - } - } - - return in_err, false - } - data := make([dynamic]string, context.temp_allocator) if len(config.workspace_folders) > 0 { if uri, ok := common.parse_uri(config.workspace_folders[0].uri, context.temp_allocator); ok { - filepath.walk(uri.path, walk_proc, &data) + append_packages(uri.path, &data, context.temp_allocator) } } @@ -148,7 +130,7 @@ check :: proc(paths: []string, uri: common.Uri, config: ^common.Config) { entry_point_opt, config.checker_args, "-json-errors", - ODIN_OS == .Linux || ODIN_OS == .Darwin ? "2>&1" : "", + ODIN_OS in runtime.Odin_OS_Types{.Linux, .Darwin, .FreeBSD, .OpenBSD, .NetBSD} ? "2>&1" : "", ), &data, ); !ok { diff --git a/src/server/clone.odin b/src/server/clone.odin index 215ab42..5a9a3ba 100644 --- a/src/server/clone.odin +++ b/src/server/clone.odin @@ -1,3 +1,4 @@ +#+feature using-stmt package server import "base:intrinsics" diff --git a/src/server/collector.odin b/src/server/collector.odin index f082b73..0f85774 100644 --- a/src/server/collector.odin +++ b/src/server/collector.odin @@ -1,5 +1,7 @@ +#+feature using-stmt package server +import "core:fmt" import "core:mem" import "core:odin/ast" import "core:path/filepath" @@ -32,10 +34,13 @@ Method :: struct { } SymbolPackage :: struct { - symbols: map[string]Symbol, - objc_structs: map[string]ObjcStruct, //mapping from struct name to function - methods: map[Method][dynamic]Symbol, - imports: [dynamic]string, //Used for references to figure whether the package is even able to reference the symbol + symbols: map[string]Symbol, + objc_structs: map[string]ObjcStruct, //mapping from struct name to function + methods: map[Method][dynamic]Symbol, + imports: [dynamic]string, //Used for references to figure whether the package is even able to reference the symbol + proc_group_members: map[string]bool, // Tracks procedure names that are part of proc groups (used by fake methods) + doc: strings.Builder, + comment: strings.Builder, } get_index_unique_string :: proc { @@ -437,46 +442,185 @@ add_comp_lit_fields :: proc( generic.ranges = ranges[:] } +/* + Records the names of procedures that are part of a proc group. + This is used by the fake methods feature to hide individual procs + when the proc group should be shown instead. +*/ +record_proc_group_members :: proc(collection: ^SymbolCollection, group: ^ast.Proc_Group, pkg_name: string) { + pkg := get_or_create_package(collection, pkg_name) + + for arg in group.args { + name := get_proc_group_member_name(arg) or_continue + pkg.proc_group_members[get_index_unique_string(collection, name)] = true + } +} + +@(private = "file") +get_proc_group_member_name :: proc(expr: ^ast.Expr) -> (name: string, ok: bool) { + #partial switch v in expr.derived { + case ^ast.Ident: + return v.name, true + case ^ast.Selector_Expr: + // For package.proc_name, we only care about the proc name + if field, is_ident := v.field.derived.(^ast.Ident); is_ident { + return field.name, true + } + } + return "", false +} + +@(private = "file") +get_or_create_package :: proc(collection: ^SymbolCollection, pkg_name: string) -> ^SymbolPackage { + pkg := &collection.packages[pkg_name] + if pkg == nil || pkg.symbols == nil { + collection.packages[pkg_name] = {} + pkg = &collection.packages[pkg_name] + pkg.symbols = make(map[string]Symbol, 100, collection.allocator) + pkg.methods = make(map[Method][dynamic]Symbol, 100, collection.allocator) + pkg.objc_structs = make(map[string]ObjcStruct, 5, collection.allocator) + pkg.proc_group_members = make(map[string]bool, 10, collection.allocator) + pkg.doc = strings.builder_make(collection.allocator) + pkg.comment = strings.builder_make(collection.allocator) + } + return pkg +} + +/* + Collects a procedure as a fake method if it's not part of a proc group. +*/ collect_method :: proc(collection: ^SymbolCollection, symbol: Symbol) { pkg := &collection.packages[symbol.pkg] - if value, ok := symbol.value.(SymbolProcedureValue); ok { - if len(value.arg_types) == 0 { - return + if symbol.name in pkg.proc_group_members { + return + } + + value, ok := symbol.value.(SymbolProcedureValue) + if !ok { + return + } + if len(value.arg_types) == 0 { + return + } + + method, method_ok := get_method_from_first_arg(collection, value.arg_types[0].type, symbol.pkg) + if !method_ok { + return + } + add_symbol_to_method(collection, pkg, method, symbol) +} + +/* + Collects a proc group as a fake method based on its member procedures' first arguments. + The proc group is registered as a method for each distinct first-argument type + across all its members. +*/ +collect_proc_group_method :: proc(collection: ^SymbolCollection, symbol: Symbol) { + pkg := &collection.packages[symbol.pkg] + + group_value, ok := symbol.value.(SymbolProcedureGroupValue) + if !ok { + return + } + + proc_group, is_proc_group := group_value.group.derived.(^ast.Proc_Group) + if !is_proc_group || len(proc_group.args) == 0 { + return + } + + // Track which method keys we've already registered to avoid duplicates + registered_methods := make(map[Method]bool, len(proc_group.args), context.temp_allocator) + + // Register the proc group as a method for each distinct first-argument type + for member_expr in proc_group.args { + member_name, name_ok := get_proc_group_member_name(member_expr) + if !name_ok { + continue } - expr, _, ok := unwrap_pointer_ident(value.arg_types[0].type) + member_symbol, found := pkg.symbols[member_name] + if !found { + continue + } - if !ok { - return + member_proc, is_proc := member_symbol.value.(SymbolProcedureValue) + if !is_proc || len(member_proc.arg_types) == 0 { + continue } - method: Method + method, method_ok := get_method_from_first_arg(collection, member_proc.arg_types[0].type, symbol.pkg) + if !method_ok { + continue + } - #partial switch v in expr.derived { - case ^ast.Selector_Expr: - if ident, ok := v.expr.derived.(^ast.Ident); ok { - method.pkg = get_index_unique_string(collection, ident.name) - method.name = get_index_unique_string(collection, v.field.name) - } else { - return - } - case ^ast.Ident: - method.pkg = symbol.pkg - method.name = get_index_unique_string(collection, v.name) - case: - return + if method not_in registered_methods { + registered_methods[method] = true + add_symbol_to_method(collection, pkg, method, symbol) } + } +} - symbols := &pkg.methods[method] +@(private = "file") +get_method_from_first_arg :: proc( + collection: ^SymbolCollection, + first_arg_type: ^ast.Expr, + default_pkg: string, +) -> ( + method: Method, + ok: bool, +) { + expr, _, unwrap_ok := unwrap_pointer_ident(first_arg_type) + if !unwrap_ok { + return {}, false + } - if symbols == nil { - pkg.methods[method] = make([dynamic]Symbol, collection.allocator) - symbols = &pkg.methods[method] + #partial switch v in expr.derived { + case ^ast.Selector_Expr: + ident, is_ident := v.expr.derived.(^ast.Ident) + if !is_ident { + return {}, false } + method.pkg = get_index_unique_string(collection, ident.name) + method.name = get_index_unique_string(collection, v.field.name) + case ^ast.Ident: + if is_builtin_type_name(v.name) { + method.pkg = "$builtin" + } else { + method.pkg = default_pkg + } + method.name = get_index_unique_string(collection, v.name) + case: + return {}, false + } - append(symbols, symbol) + return method, true +} + +is_builtin_type_name :: proc(name: string) -> bool { + for names in untyped_map { + for builtin_name in names { + if name == builtin_name { + return true + } + } + } + // Also check some other builtin types not in untyped_map + switch name { + case "rawptr", "uintptr", "typeid", "any", "rune": + return true + } + return false +} + +@(private = "file") +add_symbol_to_method :: proc(collection: ^SymbolCollection, pkg: ^SymbolPackage, method: Method, symbol: Symbol) { + symbols := &pkg.methods[method] + if symbols == nil { + pkg.methods[method] = make([dynamic]Symbol, collection.allocator) + symbols = &pkg.methods[method] } + append(symbols, symbol) } collect_objc :: proc(collection: ^SymbolCollection, attributes: []^ast.Attribute, symbol: Symbol) { @@ -523,13 +667,62 @@ collect_imports :: proc(collection: ^SymbolCollection, file: ast.File, directory } +@(private = "file") +get_symbol_package_name :: proc( + collection: ^SymbolCollection, + directory: string, + uri: string, + treat_as_builtin := false, +) -> string { + if treat_as_builtin || strings.contains(uri, "builtin.odin") { + return "$builtin" + } + + if strings.contains(uri, "intrinsics.odin") { + intrinsics_path, _ := filepath.join( + elems = {common.config.collections["base"], "/intrinsics"}, + allocator = context.temp_allocator, + ) + intrinsics_path, _ = filepath.replace_path_separators(intrinsics_path, '/', context.temp_allocator) + return get_index_unique_string(collection, intrinsics_path) + } + + return get_index_unique_string(collection, directory) +} + +@(private = "file") +get_package_decl_doc_comment :: proc(file: ast.File, allocator := context.temp_allocator) -> (string, string) { + if file.pkg_decl != nil { + docs := get_comment(file.pkg_decl.docs, allocator = allocator) + comment := get_comment(file.pkg_decl.comment, allocator = allocator) + return docs, comment + } + return "", "" +} + +@(private = "file") +write_doc_string :: proc(sb: ^strings.Builder, doc: string) { + if doc != "" { + if strings.builder_len(sb^) > 0 { + fmt.sbprintf(sb, "\n%s", doc) + } else { + strings.write_string(sb, doc) + } + } +} collect_symbols :: proc(collection: ^SymbolCollection, file: ast.File, uri: string) -> common.Error { - forward, _ := filepath.to_slash(file.fullpath, context.temp_allocator) + forward, _ := filepath.replace_path_separators(file.fullpath, '/', context.temp_allocator) directory := path.dir(forward, context.temp_allocator) package_map := get_package_mapping(file, collection.config, directory) exprs := collect_globals(file) + file_pkg_name := get_symbol_package_name(collection, directory, uri) + file_pkg := get_or_create_package(collection, file_pkg_name) + doc, comment := get_package_decl_doc_comment(file, collection.allocator) + write_doc_string(&file_pkg.doc, doc) + write_doc_string(&file_pkg.comment, comment) + for expr in exprs { symbol: Symbol @@ -554,6 +747,9 @@ collect_symbols :: proc(collection: ^SymbolCollection, file: ast.File, uri: stri } } + // Compute pkg early so it's available inside the switch + symbol.pkg = get_symbol_package_name(collection, directory, uri, expr.builtin) + #partial switch v in col_expr.derived { case ^ast.Matrix_Type: token = v^ @@ -601,6 +797,10 @@ collect_symbols :: proc(collection: ^SymbolCollection, file: ast.File, uri: stri symbol.value = SymbolProcedureGroupValue { group = clone_type(col_expr, collection.allocator, &collection.unique_strings), } + // Record proc group members for fake methods feature + if collection.config != nil && collection.config.enable_fake_method { + record_proc_group_members(collection, v, symbol.pkg) + } case ^ast.Struct_Type: token = v^ token_type = .Struct @@ -705,32 +905,23 @@ collect_symbols :: proc(collection: ^SymbolCollection, file: ast.File, uri: stri symbol.range = common.get_token_range(expr.name_expr, file.src) symbol.name = get_index_unique_string(collection, name) symbol.type = token_type - symbol.doc = get_doc(expr.docs, collection.allocator) + symbol.doc = get_comment(expr.docs, collection.allocator) symbol.uri = get_index_unique_string(collection, uri) symbol.type_expr = clone_type(expr.type_expr, collection.allocator, &collection.unique_strings) symbol.value_expr = clone_type(expr.value_expr, collection.allocator, &collection.unique_strings) comment, _ := get_file_comment(file, symbol.range.start.line + 1) - symbol.comment = strings.clone(get_comment(comment), collection.allocator) - - if expr.builtin || strings.contains(uri, "builtin.odin") { - symbol.pkg = "$builtin" - } else if strings.contains(uri, "intrinsics.odin") { - path := filepath.join( - elems = {common.config.collections["base"], "/intrinsics"}, - allocator = context.temp_allocator, - ) - - path, _ = filepath.to_slash(path, context.temp_allocator) + symbol.comment = get_comment(comment, collection.allocator) - symbol.pkg = get_index_unique_string(collection, path) - } else { - symbol.pkg = get_index_unique_string(collection, directory) - } + // symbol.pkg was already set earlier before the switch if is_distinct { symbol.flags |= {.Distinct} } + if expr.builtin { + symbol.flags |= {.Builtin} + } + if expr.deprecated { symbol.flags |= {.Deprecated} } @@ -751,25 +942,12 @@ collect_symbols :: proc(collection: ^SymbolCollection, file: ast.File, uri: stri symbol.flags |= {.Mutable} } - pkg: ^SymbolPackage - ok: bool - - if pkg, ok = &collection.packages[symbol.pkg]; !ok { - collection.packages[symbol.pkg] = {} - pkg = &collection.packages[symbol.pkg] - pkg.symbols = make(map[string]Symbol, 100, collection.allocator) - pkg.methods = make(map[Method][dynamic]Symbol, 100, collection.allocator) - pkg.objc_structs = make(map[string]ObjcStruct, 5, collection.allocator) - } + pkg := get_or_create_package(collection, symbol.pkg) if .ObjC in symbol.flags { collect_objc(collection, expr.attributes, symbol) } - if symbol.type == .Function && common.config.enable_fake_method { - collect_method(collection, symbol) - } - if v, ok := pkg.symbols[symbol.name]; !ok || v.name == "" { pkg.symbols[symbol.name] = symbol } else { @@ -777,12 +955,47 @@ collect_symbols :: proc(collection: ^SymbolCollection, file: ast.File, uri: stri } } + // Second pass: collect fake methods after all symbols and proc group members are recorded + if collection.config != nil && collection.config.enable_fake_method { + collect_fake_methods(collection, exprs, directory, uri) + } + collect_imports(collection, file, directory) return .None } +/* + Collects fake methods for all procedures and proc groups. + This is done as a second pass after all symbols are collected, + so that we know which procedures are part of proc groups. +*/ +@(private = "file") +collect_fake_methods :: proc(collection: ^SymbolCollection, exprs: []GlobalExpr, directory: string, uri: string) { + for expr in exprs { + // Determine the package name (same logic as in collect_symbols) + pkg_name := get_symbol_package_name(collection, directory, uri, expr.builtin) + + pkg, ok := &collection.packages[pkg_name] + if !ok { + continue + } + + symbol, found := pkg.symbols[expr.name] + if !found { + continue + } + + #partial switch _ in symbol.value { + case SymbolProcedureValue: + collect_method(collection, symbol) + case SymbolProcedureGroupValue: + collect_proc_group_method(collection, symbol) + } + } +} + Reference :: struct { identifiers: [dynamic]common.Location, selectors: map[string][dynamic]common.Range, diff --git a/src/server/completion.odin b/src/server/completion.odin index 8a0f371..31d75ca 100644 --- a/src/server/completion.odin +++ b/src/server/completion.odin @@ -410,6 +410,10 @@ score_completion_item :: proc(item: ^CompletionResult, curr_pkg: string) { if item.symbol.pkg == curr_pkg { item.score += 1 } + + if strings.has_prefix(item.symbol.name, "_") { + item.score -= 0.5 + } } @(private = "file") @@ -560,80 +564,25 @@ get_attribute_completion :: proc( } -DIRECTIVE_NAME_LIST :: []string { - // basic directives - "file", - "directory", - "line", - "procedure", - "caller_location", - "reverse", - // call directives - "location", - "caller_expression", - "exists", - "load", - "load_directory", - "load_hash", - "hash", - "assert", - "panic", - "defined", - "config", - /* type helper */ - "type", - /* struct type */ - "packed", - "raw_union", - "align", - "all_or_none", - /* union type */ - "no_nil", - "shared_nil", - /* array type */ - "simd", - "soa", - "sparse", - /* ptr type */ - "relative", - /* field flags */ - "no_alias", - "c_vararg", - "const", - "any_int", - "subtype", - "by_ptr", - "no_broadcast", - "no_capture", - /* swich flags */ - "partial", - /* block flags */ - "bounds_check", - "no_bounds_check", - "type_assert", - "no_type_assert", - /* proc inlining */ - "force_inline", - "force_no_inline", - /* return values flags */ - "optional_ok", - "optional_allocator_error", -} - completion_items_directives: []CompletionResult @(init) _init_completion_items_directives :: proc "contextless" () { context = runtime.default_context() - completion_items_directives = slice.mapper(DIRECTIVE_NAME_LIST, proc(name: string) -> CompletionResult { - return CompletionResult { - completion_item = CompletionItem { - detail = strings.concatenate({"#", name}) or_else name, - label = name, - kind = .Constant, + directives := make([dynamic]CompletionResult, 0, len(directive_docs), allocator = context.allocator) + for name, doc in directive_docs { + documentation := MarkupContent { + kind = "markdown", + value = doc, + } + append( + &directives, + CompletionResult { + completion_item = CompletionItem{label = name, kind = .Constant, documentation = documentation}, }, - } - }) + ) + } + completion_items_directives = directives[:] } get_directive_completion :: proc( @@ -785,7 +734,10 @@ get_selector_completion :: proc( selector.type != .Field && selector.type != .Package && selector.type != .Enum && - selector.type != .Function { + selector.type != .Function && + (selector.type == .Struct && .Variable not_in selector.flags) { + // We don't want completions for struct types, but we do want completions for constant variables. + // See tests `ast_global_non_mutable_completion` vs `ast_completion_global_selector_from_local_scope` return is_incomplete } @@ -793,7 +745,7 @@ get_selector_completion :: proc( field: string - if position_context.field != nil { + if position_context.field != nil && position_in_node(position_context.field, position_context.position) { #partial switch v in position_context.field.derived { case ^ast.Ident: field = v.name @@ -820,99 +772,7 @@ get_selector_completion :: proc( case SymbolFixedArrayValue: is_incomplete = true append_magic_array_like_completion(position_context, selector, results) - - containsColor := 1 - containsCoord := 1 - - expr_len := 0 - - if v.len != nil { - if basic, ok := v.len.derived.(^ast.Basic_Lit); ok { - if expr_len, ok = strconv.parse_int(basic.tok.text); !ok { - expr_len = 0 - } - } - } - - if field != "" { - for i := 0; i < len(field); i += 1 { - c := field[i] - if _, ok := swizzle_color_map[c]; ok { - containsColor += 1 - } else if _, ok := swizzle_coord_map[c]; ok { - containsCoord += 1 - } else { - return is_incomplete - } - } - } - - if containsColor == 1 && containsCoord == 1 { - save := expr_len - for k in swizzle_color_components { - if expr_len <= 0 { - break - } - - expr_len -= 1 - - item := CompletionItem { - label = fmt.tprintf("%v%v", field, k), - kind = .Property, - detail = fmt.tprintf("%v%v: %v", field, k, node_to_string(v.expr)), - } - append(results, CompletionResult{completion_item = item}) - } - - expr_len = save - - for k in swizzle_coord_components { - if expr_len <= 0 { - break - } - - expr_len -= 1 - - item := CompletionItem { - label = fmt.tprintf("%v%v", field, k), - kind = .Property, - detail = fmt.tprintf("%v%v: %v", field, k, node_to_string(v.expr)), - } - append(results, CompletionResult{completion_item = item}) - } - } - - if containsColor > 1 { - for k in swizzle_color_components { - if expr_len <= 0 { - break - } - - expr_len -= 1 - - item := CompletionItem { - label = fmt.tprintf("%v%v", field, k), - kind = .Property, - detail = fmt.tprintf("%v%v: [%v]%v", field, k, containsColor, node_to_string(v.expr)), - } - append(results, CompletionResult{completion_item = item}) - } - } else if containsCoord > 1 { - for k in swizzle_coord_components { - if expr_len <= 0 { - break - } - - expr_len -= 1 - - item := CompletionItem { - label = fmt.tprintf("%v%v", field, k), - kind = .Property, - detail = fmt.tprintf("%v%v: [%v]%v", field, k, containsCoord, node_to_string(v.expr)), - } - append(results, CompletionResult{completion_item = item}) - } - } + add_fixed_array_selector_completions(v, field, results) add_soa_field_completion(ast_context, selector, v.expr, v.len, results, selector.name) case SymbolUnionValue: is_incomplete = false @@ -975,6 +835,9 @@ get_selector_completion :: proc( remove_edit, rok := create_remove_edit(position_context, true) if !rok {break} + // Sublime Text will remove the original `.` for some reason + is_sublime := config.client_name == "Sublime Text LSP" + for name in enumv.names { append( results, @@ -995,7 +858,7 @@ get_selector_completion :: proc( label = fmt.tprintf(".%s in", name), kind = .EnumMember, detail = in_text, - insertText = in_text[1:], + insertText = is_sublime ? in_text : in_text[1:], additionalTextEdits = remove_edit, }, }, @@ -1008,7 +871,7 @@ get_selector_completion :: proc( label = fmt.tprintf(".%s not_in", name), kind = .EnumMember, detail = not_in_text, - insertText = not_in_text[1:], + insertText = is_sublime ? not_in_text : not_in_text[1:], additionalTextEdits = remove_edit, }, }, @@ -1019,6 +882,13 @@ get_selector_completion :: proc( is_incomplete = false for name, i in v.names { if name == "_" { + if is_struct_field_using(v, i) { + if symbol, ok := resolve_type_expression(ast_context, v.types[i]); ok { + if value, ok := symbol.value.(SymbolFixedArrayValue); ok { + add_fixed_array_selector_completions(value, field, results) + } + } + } continue } @@ -1039,6 +909,12 @@ get_selector_completion :: proc( construct_struct_field_symbol(&symbol, selector.name, v, i) append(results, CompletionResult{symbol = symbol}) + + if is_struct_field_using(v, i) { + if value, ok := symbol.value.(SymbolFixedArrayValue); ok { + add_fixed_array_selector_completions(value, field, results) + } + } } else { //just give some generic symbol with name. item := CompletionItem { @@ -1090,9 +966,17 @@ get_selector_completion :: proc( case SymbolPackageValue: is_incomplete = true - pkg := selector.pkg + packages := make([dynamic]string, context.temp_allocator) + if is_builtin_pkg(selector.pkg) { + append(&packages, "$builtin") + for built in indexer.builtin_packages { + append(&packages, built) + } + } else { + append(&packages, selector.pkg) + } - if searched, ok := fuzzy_search(field, {pkg}, ast_context.fullpath); ok { + if searched, ok := fuzzy_search(field, packages[:], ast_context.fullpath); ok { for search in searched { symbol := search.symbol @@ -1132,6 +1016,105 @@ get_selector_completion :: proc( return is_incomplete } +add_fixed_array_selector_completions :: proc( + v: SymbolFixedArrayValue, + field: string, + results: ^[dynamic]CompletionResult, +) { + containsColor := 1 + containsCoord := 1 + + expr_len := 0 + + if v.len != nil { + if basic, ok := v.len.derived.(^ast.Basic_Lit); ok { + if expr_len, ok = strconv.parse_int(basic.tok.text); !ok { + expr_len = 0 + } + } + } + + if field != "" { + for i := 0; i < len(field); i += 1 { + c := field[i] + if _, ok := swizzle_color_map[c]; ok { + containsColor += 1 + } else if _, ok := swizzle_coord_map[c]; ok { + containsCoord += 1 + } else { + return + } + } + } + + if containsColor == 1 && containsCoord == 1 { + save := expr_len + for k in swizzle_color_components { + if expr_len <= 0 { + break + } + + expr_len -= 1 + + item := CompletionItem { + label = fmt.tprintf("%v%v", field, k), + kind = .Property, + detail = fmt.tprintf("%v%v: %v", field, k, node_to_string(v.expr)), + } + append(results, CompletionResult{completion_item = item}) + } + + expr_len = save + + for k in swizzle_coord_components { + if expr_len <= 0 { + break + } + + expr_len -= 1 + + item := CompletionItem { + label = fmt.tprintf("%v%v", field, k), + kind = .Property, + detail = fmt.tprintf("%v%v: %v", field, k, node_to_string(v.expr)), + } + append(results, CompletionResult{completion_item = item}) + } + } + + if containsColor > 1 { + for k in swizzle_color_components { + if expr_len <= 0 { + break + } + + expr_len -= 1 + + item := CompletionItem { + label = fmt.tprintf("%v%v", field, k), + kind = .Property, + detail = fmt.tprintf("%v%v: [%v]%v", field, k, containsColor, node_to_string(v.expr)), + } + append(results, CompletionResult{completion_item = item}) + } + } else if containsCoord > 1 { + for k in swizzle_coord_components { + if expr_len <= 0 { + break + } + + expr_len -= 1 + + item := CompletionItem { + label = fmt.tprintf("%v%v", field, k), + kind = .Property, + detail = fmt.tprintf("%v%v: [%v]%v", field, k, containsCoord, node_to_string(v.expr)), + } + append(results, CompletionResult{completion_item = item}) + } + } +} + get_implicit_completion :: proc( ast_context: ^AstContext, position_context: ^DocumentPositionContext, @@ -1254,35 +1237,6 @@ get_implicit_completion :: proc( } } - if position_context.assign != nil && - position_context.assign.lhs != nil && - len(position_context.assign.lhs) == 1 && - is_bitset_assignment_operator(position_context.assign.op.text) { - //bitsets - if symbol, ok := resolve_type_expression(ast_context, position_context.assign.lhs[0]); ok { - set_ast_package_set_scoped(ast_context, symbol.pkg) - if value, ok := unwrap_bitset(ast_context, symbol); ok { - for name in value.names { - if position_context.comp_lit != nil && field_exists_in_comp_lit(position_context.comp_lit, name) { - continue - } - - item := CompletionItem { - label = name, - kind = .EnumMember, - detail = name, - } - - append(results, CompletionResult{completion_item = item}) - } - - return is_incomplete - } - } - - reset_ast_context(ast_context) - } - if position_context.comp_lit != nil && position_context.parent_binary != nil && is_bitset_binary_operator(position_context.binary.op.text) { @@ -1380,53 +1334,6 @@ get_implicit_completion :: proc( } } - if position_context.assign != nil && position_context.assign.rhs != nil && position_context.assign.lhs != nil { - rhs_index: int - - for elem in position_context.assign.rhs { - if position_in_node(elem, position_context.position) { - break - } else { - //procedures are the only types that can return more than one value - if symbol, ok := resolve_type_expression(ast_context, elem); ok { - if procedure, ok := symbol.value.(SymbolProcedureValue); ok { - if procedure.return_types == nil { - return is_incomplete - } - - rhs_index += len(procedure.return_types) - } else { - rhs_index += 1 - } - } - } - } - - if len(position_context.assign.lhs) > rhs_index { - if enum_value, unwrapped_super_enum, ok := unwrap_enum( - ast_context, - position_context.assign.lhs[rhs_index], - ); ok { - for name in enum_value.names { - item := CompletionItem { - label = name, - kind = .EnumMember, - detail = name, - } - if unwrapped_super_enum { - add_implicit_selector_remove_edit(position_context, &item, name, enum_value.names) - } - - append(results, CompletionResult{completion_item = item}) - } - - return is_incomplete - } - } - - reset_ast_context(ast_context) - } - if position_context.returns != nil && position_context.function != nil { return_index: int @@ -1527,6 +1434,11 @@ get_implicit_completion :: proc( if position_context.call != nil { if call, ok := position_context.call.derived.(^ast.Call_Expr); ok { parameter_index, parameter_ok := find_position_in_call_param(position_context, call^) + old := ast_context.resolve_specific_overload + ast_context.resolve_specific_overload = true + defer { + ast_context.resolve_specific_overload = old + } if symbol, ok := resolve_type_expression(ast_context, call.expr); ok && parameter_ok { set_ast_package_set_scoped(ast_context, symbol.pkg) @@ -1559,6 +1471,8 @@ get_implicit_completion :: proc( type = comp_lit.type } else if selector, ok := arg_type.default_value.derived.(^ast.Selector_Expr); ok { type = selector.expr + } else { + type = arg_type.default_value } } @@ -1599,6 +1513,83 @@ get_implicit_completion :: proc( reset_ast_context(ast_context) } + + if position_context.assign != nil && + position_context.assign.lhs != nil && + len(position_context.assign.lhs) == 1 && + is_bitset_assignment_operator(position_context.assign.op.text) { + //bitsets + if symbol, ok := resolve_type_expression(ast_context, position_context.assign.lhs[0]); ok { + set_ast_package_set_scoped(ast_context, symbol.pkg) + if value, ok := unwrap_bitset(ast_context, symbol); ok { + for name in value.names { + if position_context.comp_lit != nil && field_exists_in_comp_lit(position_context.comp_lit, name) { + continue + } + + item := CompletionItem { + label = name, + kind = .EnumMember, + detail = name, + } + + append(results, CompletionResult{completion_item = item}) + } + + return is_incomplete + } + } + + reset_ast_context(ast_context) + } + + if position_context.assign != nil && position_context.assign.rhs != nil && position_context.assign.lhs != nil { + rhs_index: int + + for elem in position_context.assign.rhs { + if position_in_node(elem, position_context.position) { + break + } else { + //procedures are the only types that can return more than one value + if symbol, ok := resolve_type_expression(ast_context, elem); ok { + if procedure, ok := symbol.value.(SymbolProcedureValue); ok { + if procedure.return_types == nil { + return is_incomplete + } + + rhs_index += len(procedure.return_types) + } else { + rhs_index += 1 + } + } + } + } + + if len(position_context.assign.lhs) > rhs_index { + if enum_value, unwrapped_super_enum, ok := unwrap_enum( + ast_context, + position_context.assign.lhs[rhs_index], + ); ok { + for name in enum_value.names { + item := CompletionItem { + label = name, + kind = .EnumMember, + detail = name, + } + if unwrapped_super_enum { + add_implicit_selector_remove_edit(position_context, &item, name, enum_value.names) + } + + append(results, CompletionResult{completion_item = item}) + } + + return is_incomplete + } + } + + reset_ast_context(ast_context) + } + return is_incomplete } @@ -1821,6 +1812,12 @@ get_identifier_completion :: proc( symbol := Symbol { name = pkg.base, type = .Package, + pkg = pkg.name, + value = SymbolPackageValue{}, + } + try_build_package(symbol.pkg) + if resolved, ok := resolve_symbol_return(ast_context, symbol); ok { + symbol = resolved } if score, ok := common.fuzzy_match(matcher, symbol.name); ok == 1 { @@ -1881,7 +1878,7 @@ get_package_completion :: proc( c := without_quotes[0:colon_index] if colon_index + 1 < len(without_quotes) { - absolute_path = filepath.join( + absolute_path, _ = filepath.join( elems = { config.collections[c], filepath.dir(without_quotes[colon_index + 1:], context.temp_allocator), @@ -1894,7 +1891,7 @@ get_package_completion :: proc( } else { import_file_dir := filepath.dir(position_context.import_stmt.pos.file, context.temp_allocator) import_dir := filepath.dir(without_quotes, context.temp_allocator) - absolute_path = filepath.join(elems = {import_file_dir, import_dir}, allocator = context.temp_allocator) + absolute_path, _ = filepath.join(elems = {import_file_dir, import_dir}, allocator = context.temp_allocator) } if !strings.contains(position_context.import_stmt.fullpath, "/") && @@ -1944,7 +1941,7 @@ search_for_packages :: proc(fullpath: string) -> []string { if files, err := os.read_dir(fh, 0, context.temp_allocator); err == 0 { for file in files { - if file.is_dir { + if file.type == .Directory { append(&packages, file.fullpath) } } @@ -1960,12 +1957,52 @@ get_used_switch_name :: proc(node: ^ast.Expr) -> (string, bool) { return n.name, true case ^ast.Selector_Expr: return n.field.name, true + case ^ast.Implicit_Selector_Expr: + return n.field.name, true case ^ast.Pointer_Type: return get_used_switch_name(n.elem) } return "", false } +//handles pointers / packages +get_qualified_union_case_name :: proc( + symbol: ^Symbol, + ast_context: ^AstContext, + position_context: ^DocumentPositionContext, +) -> string { + sb := strings.builder_make(context.temp_allocator) + pointer_prefix := repeat("^", symbol.pointers, ast_context.allocator) + strings.write_string(&sb, pointer_prefix) + if symbol.pkg != ast_context.document_package { + strings.write_string(&sb, get_symbol_pkg_name(ast_context, symbol)) + strings.write_string(&sb, ".") + } + strings.write_string(&sb, symbol.name) + #partial switch v in symbol.value { + case SymbolUnionValue: + write_poly_names(&sb, v.poly_names) + case SymbolStructValue: + write_poly_names(&sb, v.poly_names) + } + + return strings.to_string(sb) +} + +write_poly_names :: proc(sb: ^strings.Builder, poly_names: []string) { + if len(poly_names) > 0 { + strings.write_string(sb, "(") + for name, i in poly_names { + strings.write_string(sb, name) + if i != len(poly_names) - 1 { + strings.write_string(sb, ", ") + } + } + strings.write_string(sb, ")") + } +} + + get_type_switch_completion :: proc( ast_context: ^AstContext, position_context: ^DocumentPositionContext, @@ -1994,7 +2031,8 @@ get_type_switch_completion :: proc( if union_value, ok := unwrap_union(ast_context, assign.rhs[0]); ok { for type, i in union_value.types { if symbol, ok := resolve_type_expression(ast_context, union_value.types[i]); ok { - + //TODO: using symbol.name is wrong for anonymous enums and structs, where the name field is "enum" or "struct" respectively but we want to use the full signature + //we also can't use the signature all the time because type aliases need to use specifically the alias name here and not the signature name := symbol.name if _, ok := used_unions[name]; ok { continue @@ -2003,19 +2041,8 @@ get_type_switch_completion :: proc( item := CompletionItem { kind = .EnumMember, } - - if symbol.pkg == ast_context.document_package { - item.label = fmt.aprintf("%v%v", repeat("^", symbol.pointers, context.temp_allocator), name) - item.detail = item.label - } else { - item.label = fmt.aprintf( - "%v%v.%v", - repeat("^", symbol.pointers, context.temp_allocator), - get_symbol_pkg_name(ast_context, &symbol), - name, - ) - item.detail = item.label - } + item.label = get_qualified_union_case_name(&symbol, ast_context, position_context) + item.detail = item.label if position_context.implicit_selector_expr != nil { if remove_edit, ok := create_implicit_selector_remove_edit(position_context); ok { item.additionalTextEdits = remove_edit diff --git a/src/server/definition.odin b/src/server/definition.odin index c793f32..7ff32d8 100644 --- a/src/server/definition.odin +++ b/src/server/definition.odin @@ -2,17 +2,9 @@ package server import "core:fmt" import "core:log" -import "core:mem" import "core:odin/ast" -import "core:odin/parser" import "core:odin/tokenizer" -import "core:os" import "core:path/filepath" -import path "core:path/slashpath" -import "core:slice" -import "core:sort" -import "core:strconv" -import "core:strings" import "src:common" @@ -42,7 +34,7 @@ get_all_package_file_locations :: proc( return true } -get_definition_location :: proc(document: ^Document, position: common.Position) -> ([]common.Location, bool) { +get_definition_location :: proc(document: ^Document, position: common.Position, config: ^common.Config) -> ([]common.Location, bool) { locations := make([dynamic]common.Location, context.temp_allocator) location: common.Location @@ -100,6 +92,14 @@ get_definition_location :: proc(document: ^Document, position: common.Position) } if resolved, ok := resolve_location_selector(&ast_context, position_context.selector_expr); ok { + if config.enable_overload_resolution { + resolved = try_resolve_proc_group_overload( + &ast_context, + &position_context, + resolved, + position_context.selector_expr, + ) + } location.range = resolved.range uri = resolved.uri } else { @@ -139,12 +139,12 @@ get_definition_location :: proc(document: ^Document, position: common.Position) &ast_context, position_context.identifier.derived.(^ast.Ident)^, ); ok { + if config.enable_overload_resolution { + resolved = try_resolve_proc_group_overload(&ast_context, &position_context, resolved) + } if v, ok := resolved.value.(SymbolAggregateValue); ok { for symbol in v.symbols { - append(&locations, common.Location { - range = symbol.range, - uri = symbol.uri, - }) + append(&locations, common.Location{range = symbol.range, uri = symbol.uri}) } } location.range = resolved.range @@ -167,3 +167,114 @@ get_definition_location :: proc(document: ^Document, position: common.Position) return locations[:], true } + + +try_resolve_proc_group_overload :: proc( + ast_context: ^AstContext, + position_context: ^DocumentPositionContext, + symbol: Symbol, + selector_expr: ^ast.Node = nil, +) -> Symbol { + if position_context.call == nil { + return symbol + } + + call, is_call := position_context.call.derived.(^ast.Call_Expr) + if !is_call { + return symbol + } + + if position_in_exprs(call.args, position_context.position) { + return symbol + } + + // For selector expressions, we need to look up the full symbol to check if it's a proc group + full_symbol := symbol + if result, ok := get_full_symbol_from_selector(ast_context, selector_expr, symbol); ok { + full_symbol = result + } else if result, ok := get_full_symbol_from_identifier(ast_context, position_context, symbol); ok { + full_symbol = result + } + + proc_group_value, is_proc_group := full_symbol.value.(SymbolProcedureGroupValue) + if !is_proc_group { + return symbol + } + + old_call := ast_context.call + ast_context.call = call + defer { + ast_context.call = old_call + } + + if resolved, ok := resolve_function_overload(ast_context, proc_group_value.group.derived.(^ast.Proc_Group)); ok { + if resolved.name != "" { + if global, ok := ast_context.globals[resolved.name]; ok { + resolved.range = common.get_token_range(global.name_expr, ast_context.file.src) + resolved.uri = common.create_uri(global.name_expr.pos.file, ast_context.allocator).uri + } else if indexed_symbol, ok := lookup(resolved.name, resolved.pkg, ast_context.fullpath); ok { + resolved.range = indexed_symbol.range + resolved.uri = indexed_symbol.uri + } + } + return resolved + } + + return symbol +} + +get_full_symbol_from_selector :: proc( + ast_context: ^AstContext, + selector_expr: ^ast.Node, + symbol: Symbol, +) -> ( + full_symbol: Symbol, + ok: bool, +) { + if selector_expr == nil do return + + selector := selector_expr.derived.(^ast.Selector_Expr) or_return + + _, is_pkg := symbol.value.(SymbolPackageValue) + if !is_pkg && symbol.value != nil do return + + if selector.field == nil do return + + ident := selector.field.derived.(^ast.Ident) or_return + + return lookup(ident.name, symbol.pkg, ast_context.fullpath); +} + +get_full_symbol_from_identifier :: proc( + ast_context: ^AstContext, + position_context: ^DocumentPositionContext, + symbol: Symbol, +) -> ( + full_symbol: Symbol, + ok: bool, +) { + if position_context.identifier == nil || symbol.value != nil do return + + // For identifiers (non-selector), the symbol from resolve_location_identifier may not have + // value set (e.g., for globals). We need to do a lookup to get the full symbol. + ident := position_context.identifier.derived.(^ast.Ident) or_return + + pkg := symbol.pkg if symbol.pkg != "" else ast_context.document_package + + if pkg_symbol, ok := lookup(ident.name, pkg, ast_context.fullpath); ok { + return pkg_symbol, true + } + + // If lookup fails (e.g., in tests without full indexing), try checking if it's a proc group + + global := ast_context.globals[ident.name] or_return + if proc_group, is_proc_group := global.expr.derived.(^ast.Proc_Group); is_proc_group { + full_symbol = symbol + full_symbol.value = SymbolProcedureGroupValue { + group = global.expr, + } + return full_symbol, true + } + + return Symbol{}, false +} diff --git a/src/server/documentation.odin b/src/server/documentation.odin index 0069ac4..7c26a37 100644 --- a/src/server/documentation.odin +++ b/src/server/documentation.odin @@ -6,112 +6,9 @@ import "core:odin/ast" import path "core:path/slashpath" import "core:strings" -// Docs taken from https://pkg.odin-lang.org/base/builtin -keywords_docs: map[string]string = { - "typeid" = "```odin\ntypeid :: typeid\n```\n`typeid` is a unique identifier for an Odin type at runtime. It can be mapped to relevant type information through `type_info_of`.", - "string" = "```odin\nstring :: string\n```\n`string` is the set of all strings of 8-bit bytes, conventionally but not necessarily representing UTF-8 encoding text. A `string` may be empty but not `nil`. Elements of `string` type are immutable and indexable.", - "string16" = "", - "cstring" = "```odin\ncstring :: cstring\n```\n`cstring` is the set of all strings of 8-bit bytes terminated with a NUL (0) byte, conventionally but not necessarily representing UTF-8 encoding text. A `cstring` may be empty or `nil`. Elements of `cstring` type are immutable but not indexable.", - "cstring16" = "", - "int" = "```odin\nint :: int\n```\n`int` is a signed integer type that is at least 32 bits in size. It is a distinct type, however, and not an alias for say, `i32`.", - "uint" = "```odin\nuint :: uint\n```\n`uint` is an unsigned integer type that is at least 32 bits in size. It is a distinct type, however, and not an alias for say, `u32`.", - "u8" = "```odin\nu8 :: u8\n```\n`u8` is the set of all unsigned 8-bit integers. Range 0 through 255.", - "i8" = "```odin\ni8 :: i8\n```\n`i8` is the set of all signed 8-bit integers. Range -128 through 127.", - "u16" = "```odin\nu16 :: u16\n```\n`u16` is the set of all unsigned 16-bit integers with native endianness. Range 0 through 65535.", - "i16" = "```odin\ni16 :: i16\n```\n`i16` is the set of all signed 16-bit integers with native endianness. Range -32768 through 32767.", - "u32" = "```odin\nu32 :: u32\n```\n`u32` is the set of all unsigned 32-bit integers with native endianness. Range 0 through 4294967295.", - "i32" = "```odin\ni32 :: i32\n```\n`i32` is the set of all signed 32-bit integers with native endianness. Range -2147483648 through 2147483647.", - "u64" = "```odin\nu64 :: u64\n```\n`u64` is the set of all unsigned 64-bit integers with native endianness. Range 0 through 18446744073709551615.", - "i64" = "```odin\ni64 :: i64\n```\n`i64` is the set of all signed 64-bit integers with native endianness. Range -9223372036854775808 through 9223372036854775807.", - "u128" = "```odin\nu128 :: u128\n```\n`u128` is the set of all unsigned 128-bit integers with native endianness. Range 0 through 340282366920938463463374607431768211455.", - "i128" = "```odin\ni128 :: i128\n```\n`i128` is the set of all signed 128-bit integers with native endianness. Range -170141183460469231731687303715884105728 through 170141183460469231731687303715884105727.", - "f16" = "```odin\nf16 :: f16\n```\n`f16` is the set of all IEEE-754 16-bit floating-point numbers with native endianness.", - "f32" = "```odin\nf32 :: f32\n```\n`f32` is the set of all IEEE-754 32-bit floating-point numbers with native endianness.", - "f64" = "```odin\nf64 :: f64\n```\n`f64` is the set of all IEEE-754 64-bit floating-point numbers with native endianness.", - "bool" = "```odin\nbool :: bool\n```\n`bool` is the set of boolean values, `false` and `true`. This is distinct to `b8`. `bool` has a size of 1 byte (8 bits).", - "any" = "```odin\nany :: any\n```\n`any` is reference any data type at runtime. Internally it contains a pointer to the underlying data and its relevant `typeid`. This is a very useful construct in order to have a runtime type safe printing procedure.\n\nNote: The `any` value is only valid for as long as the underlying data is still valid. Passing a literal to an `any` will allocate the literal in the current stack frame.\n\nNote: It is highly recommend that you do not use this unless you know what you are doing. Its primary use is for printing procedures.", - "b8" = "```odin\nb8 :: b8\n```\n`b8` is the set of boolean values, `false` and `true`. This is distinct to `bool`. `b8` has a size of 1 byte (8 bits).", - "b16" = "```odin\nb16 :: b16\n```\n`b16` is the set of boolean values, `false` and `true`. `b16` has a size of 2 bytes (16 bits).", - "b32" = "```odin\nb32 :: b32\n```\n`b32` is the set of boolean values, `false` and `true`. `b32` has a size of 4 bytes (32 bits).", - "b64" = "```odin\nb64 :: b64\n```\n`b64` is the set of boolean values, `false` and `true`. `b64` has a size of 8 bytes (64 bits).", - "true" = "```odin\ntrue :: 0 == 0 // untyped boolean\n```", - "false" = "```odin\nfalse :: 0 != 0 // untyped boolean\n```", - "nil" = "```odin\nnil :: ... // untyped nil \n```\n`nil` is a predeclared identifier representing the zero value for a pointer, multi-pointer, enum, bit_set, slice, dynamic array, map, procedure, any, typeid, cstring, union, #soa array, #soa pointer, #relative type.", - "byte" = "```odin\nbyte :: u8\n```\n`byte` is an alias for `u8` and is equivalent to `u8` in all ways. It is used as a convention to distinguish values from 8-bit unsigned integer values.", - "rune" = "```odin\nrune :: rune\n```\n`rune` is the set of all Unicode code points. It is internally the same as i32 but distinct.", - "f16be" = "```odin\nf16be :: f16be\n```\n`f16be` is the set of all IEEE-754 16-bit floating-point numbers with big endianness.", - "f16le" = "```odin\nf16le :: f16le\n```\n`f16le` is the set of all IEEE-754 16-bit floating-point numbers with little endianness.", - "f32be" = "```odin\nf32be :: f32be\n```\n`f32be` is the set of all IEEE-754 32-bit floating-point numbers with big endianness.", - "f32le" = "```odin\nf32le :: f32le\n```\n`f32le` is the set of all IEEE-754 32-bit floating-point numbers with little endianness.", - "f64be" = "```odin\nf64be :: f64be\n```\n`f64be` is the set of all IEEE-754 64-bit floating-point numbers with big endianness.", - "f64le" = "```odin\nf64le :: f64le\n```\n`f64le` is the set of all IEEE-754 64-bit floating-point numbers with little endianness.", - "i16be" = "```odin\ni16be :: i16be\n```\n`i16be` is the set of all signed 16-bit integers with big endianness. Range -32768 through 32767.", - "i16le" = "```odin\ni16le :: i16le\n```\n`i16le` is the set of all signed 16-bit integers with little endianness. Range -32768 through 32767.", - "i32be" = "```odin\ni32be :: i32be\n```\n`i32be` is the set of all signed 32-bit integers with big endianness. Range -2147483648 through 2147483647.", - "i32le" = "```odin\ni32le :: i32le\n```\n`i32le` is the set of all signed 32-bit integers with little endianness. Range -2147483648 through 2147483647.", - "i64be" = "```odin\ni64be :: i64be\n```\n`i64be` is the set of all signed 64-bit integers with big endianness. Range -9223372036854775808 through 9223372036854775807.", - "i64le" = "```odin\ni64le :: i64le\n```\n`i64le` is the set of all signed 64-bit integers with little endianness. Range -9223372036854775808 through 9223372036854775807.", - "u16be" = "```odin\nu16be :: u16be\n```\n`u16be` is the set of all unsigned 16-bit integers with big endianness. Range 0 through 65535.", - "u16le" = "```odin\nu16le :: u16le\n```\n`u16le` is the set of all unsigned 16-bit integers with little endianness. Range 0 through 65535.", - "u32be" = "```odin\nu32be :: u32be\n```\n`u32be` is the set of all unsigned 32-bit integers with big endianness. Range 0 through 4294967295.", - "u32le" = "```odin\nu32le :: u32le\n```\n`u32le` is the set of all unsigned 32-bit integers with little endianness. Range 0 through 4294967295.", - "u64be" = "```odin\nu64be :: u64be\n```\n`u64be` is the set of all unsigned 64-bit integers with big endianness. Range 0 through 18446744073709551615.", - "u64le" = "```odin\nu64le :: u64le\n```\n`u64le` is the set of all unsigned 64-bit integers with little endianness. Range 0 through 18446744073709551615.", - "i128be" = "```odin\ni128be :: i128be\n```\n`i128be` is the set of all signed 128-bit integers with big endianness. Range -170141183460469231731687303715884105728 through 170141183460469231731687303715884105727.", - "i128le" = "```odin\ni128le :: i128le\n```\n`i128le` is the set of all signed 128-bit integers with little endianness. Range -170141183460469231731687303715884105728 through 170141183460469231731687303715884105727.", - "u128be" = "```odin\nu128be :: u128be\n```\n`u128be` is the set of all unsigned 128-bit integers with big endianness. Range 0 through 340282366920938463463374607431768211455.", - "u128le" = "```odin\nu128le :: u128le\n```\n`u128le` is the set of all unsigned 128-bit integers with little endianness. Range 0 through 340282366920938463463374607431768211455.", - "complex32" = "```odin\ncomplex32 :: complex32\n```\n`complex32` is the set of all complex numbers with `f16` real and imaginary parts.", - "complex64" = "```odin\ncomplex64 :: complex64\n```\n`complex64` is the set of all complex numbers with `f32` real and imaginary parts.", - "complex128" = "```odin\ncomplex128 :: complex128\n```\n`complex128` is the set of all complex numbers with `f64` real and imaginary parts.", - "quaternion64" = "```odin\nquaternion64 :: quaternion64\n```\n`quaternion64` is the set of all complex numbers with `f16` real and imaginary (i, j, & k) parts.", - "quaternion128" = "```odin\nquaternion128 :: quaternion128\n```\n`quaternion128` is the set of all complex numbers with `f32` real and imaginary (i, j, & k) parts.", - "quaternion256" = "```odin\nquaternion256 :: quaternion256\n```\n`quaternion256` is the set of all complex numbers with `f64` real and imaginary (i, j, & k) parts.", - "uintptr" = "```odin\nuintptr :: uintptr\n```\n`uintptr` is an unsigned integer type that is large enough to hold the bit pattern of any pointer.", - "rawptr" = "```odin\nrawptr :: rawptr\n```\n`rawptr` is a pointer to an arbitrary type. It is equivalent to void * in C.", - // taken from https://github.com/odin-lang/Odin/wiki/Keywords-and-Operators - "asm" = "", - "auto_cast" = "```odin\nauto_cast v```\nAutomatically casts an expression `v` to the destination’s type if possible.", - "bit_field" = "", - "bit_set" = "", - "break" = "", - "case" = "", - "cast" = "```odin\ncast(T)v\n```\nConverts the value `v` to the type `T`.", - "const" = "", - "context" = "```odin\nruntime.context: Context\n```\nThe context variable is local to each scope. It is copy-on-write and is implicitly passed by pointer to any procedure call in that scope (if the procedure has the Odin calling convention).", - "continue" = "", - "defer" = "", - "distinct" = "```odin\ndistinct T\n```\nCreate a new type with the same underlying semantics as `T`", - "do" = "", - "dynamic" = "", - "else" = "", - "enum" = "", - "fallthrough" = "", - "for" = "", - "foreign" = "", - "if" = "", - "import" = "", - "in" = "", - "inline" = "", - "map" = "", - "not_in" = "", - "or_break" = "", - "or_continue" = "", - "or_else" = "", - "or_return" = "", - "opaque" = "", - "package" = "", - "proc" = "", - "return" = "", - "struct" = "", - "switch" = "", - "transmute" = "```odin\ntransmute(T)v\n```\nBitwise cast between 2 types of the same size.", - "typeid" = "", - "union" = "", - "using" = "", - "when" = "", - "where" = "", -} +DOC_SECTION_DELIMITER :: "\n---\n" // The string separating each section of documentation +DOC_FMT_ODIN :: "```odin\n%v\n```" // The format for wrapping odin code in a markdown codeblock +DOC_FMT_MARKDOWN :: DOC_FMT_ODIN + DOC_SECTION_DELIMITER + "%v" // The format for presenting documentation on hover // Adds signature and docs information to the provided symbol // This should only be used for a symbol created with the temp allocator @@ -127,21 +24,17 @@ build_documentation :: proc(ast_context: ^AstContext, symbol: ^Symbol, short_sig } } -construct_symbol_docs :: proc(symbol: Symbol, markdown := true, allocator := context.temp_allocator) -> string { +construct_symbol_docs :: proc(symbol: Symbol, allocator := context.temp_allocator) -> string { sb := strings.builder_make(allocator = allocator) if symbol.doc != "" { strings.write_string(&sb, symbol.doc) - if symbol.comment != "" { - strings.write_string(&sb, "\n") - } } if symbol.comment != "" { - if markdown { - fmt.sbprintf(&sb, "\n```odin\n%s\n```", symbol.comment) - } else { - fmt.sbprintf(&sb, "\n%s", symbol.comment) + if symbol.doc != "" { + strings.write_string(&sb, DOC_SECTION_DELIMITER) } + strings.write_string(&sb, symbol.comment) } return strings.to_string(sb) @@ -955,6 +848,9 @@ write_symbol_name :: proc(sb: ^strings.Builder, symbol: Symbol) { } else if pkg != "" && pkg != "$builtin" { fmt.sbprintf(sb, "%v.", pkg) } + if .PolyType in symbol.flags { + strings.write_string(sb, "$") + } strings.write_string(sb, symbol.name) } @@ -1006,3 +902,155 @@ write_symbol_type_information :: proc(sb: ^strings.Builder, ast_context: ^AstCon } return true } + +// Docs taken from https://pkg.odin-lang.org/base/builtin +keywords_docs: map[string]string = { + "typeid" = "```odin\ntypeid :: typeid\n```\n`typeid` is a unique identifier for an Odin type at runtime. It can be mapped to relevant type information through `type_info_of`.", + "string" = "```odin\nstring :: string\n```\n`string` is the set of all strings of 8-bit bytes, conventionally but not necessarily representing UTF-8 encoding text. A `string` may be empty but not `nil`. Elements of `string` type are immutable and indexable.", + "string16" = "", + "cstring" = "```odin\ncstring :: cstring\n```\n`cstring` is the set of all strings of 8-bit bytes terminated with a NUL (0) byte, conventionally but not necessarily representing UTF-8 encoding text. A `cstring` may be empty or `nil`. Elements of `cstring` type are immutable but not indexable.", + "cstring16" = "", + "int" = "```odin\nint :: int\n```\n`int` is a signed integer type that is at least 32 bits in size. It is a distinct type, however, and not an alias for say, `i32`.", + "uint" = "```odin\nuint :: uint\n```\n`uint` is an unsigned integer type that is at least 32 bits in size. It is a distinct type, however, and not an alias for say, `u32`.", + "u8" = "```odin\nu8 :: u8\n```\n`u8` is the set of all unsigned 8-bit integers. Range 0 through 255.", + "i8" = "```odin\ni8 :: i8\n```\n`i8` is the set of all signed 8-bit integers. Range -128 through 127.", + "u16" = "```odin\nu16 :: u16\n```\n`u16` is the set of all unsigned 16-bit integers with native endianness. Range 0 through 65535.", + "i16" = "```odin\ni16 :: i16\n```\n`i16` is the set of all signed 16-bit integers with native endianness. Range -32768 through 32767.", + "u32" = "```odin\nu32 :: u32\n```\n`u32` is the set of all unsigned 32-bit integers with native endianness. Range 0 through 4294967295.", + "i32" = "```odin\ni32 :: i32\n```\n`i32` is the set of all signed 32-bit integers with native endianness. Range -2147483648 through 2147483647.", + "u64" = "```odin\nu64 :: u64\n```\n`u64` is the set of all unsigned 64-bit integers with native endianness. Range 0 through 18446744073709551615.", + "i64" = "```odin\ni64 :: i64\n```\n`i64` is the set of all signed 64-bit integers with native endianness. Range -9223372036854775808 through 9223372036854775807.", + "u128" = "```odin\nu128 :: u128\n```\n`u128` is the set of all unsigned 128-bit integers with native endianness. Range 0 through 340282366920938463463374607431768211455.", + "i128" = "```odin\ni128 :: i128\n```\n`i128` is the set of all signed 128-bit integers with native endianness. Range -170141183460469231731687303715884105728 through 170141183460469231731687303715884105727.", + "f16" = "```odin\nf16 :: f16\n```\n`f16` is the set of all IEEE-754 16-bit floating-point numbers with native endianness.", + "f32" = "```odin\nf32 :: f32\n```\n`f32` is the set of all IEEE-754 32-bit floating-point numbers with native endianness.", + "f64" = "```odin\nf64 :: f64\n```\n`f64` is the set of all IEEE-754 64-bit floating-point numbers with native endianness.", + "bool" = "```odin\nbool :: bool\n```\n`bool` is the set of boolean values, `false` and `true`. This is distinct to `b8`. `bool` has a size of 1 byte (8 bits).", + "any" = "```odin\nany :: any\n```\n`any` is reference any data type at runtime. Internally it contains a pointer to the underlying data and its relevant `typeid`. This is a very useful construct in order to have a runtime type safe printing procedure.\n\nNote: The `any` value is only valid for as long as the underlying data is still valid. Passing a literal to an `any` will allocate the literal in the current stack frame.\n\nNote: It is highly recommend that you do not use this unless you know what you are doing. Its primary use is for printing procedures.", + "b8" = "```odin\nb8 :: b8\n```\n`b8` is the set of boolean values, `false` and `true`. This is distinct to `bool`. `b8` has a size of 1 byte (8 bits).", + "b16" = "```odin\nb16 :: b16\n```\n`b16` is the set of boolean values, `false` and `true`. `b16` has a size of 2 bytes (16 bits).", + "b32" = "```odin\nb32 :: b32\n```\n`b32` is the set of boolean values, `false` and `true`. `b32` has a size of 4 bytes (32 bits).", + "b64" = "```odin\nb64 :: b64\n```\n`b64` is the set of boolean values, `false` and `true`. `b64` has a size of 8 bytes (64 bits).", + "true" = "```odin\ntrue :: 0 == 0 // untyped boolean\n```", + "false" = "```odin\nfalse :: 0 != 0 // untyped boolean\n```", + "nil" = "```odin\nnil :: ... // untyped nil \n```\n`nil` is a predeclared identifier representing the zero value for a pointer, multi-pointer, enum, bit_set, slice, dynamic array, map, procedure, any, typeid, cstring, union, #soa array, #soa pointer, #relative type.", + "byte" = "```odin\nbyte :: u8\n```\n`byte` is an alias for `u8` and is equivalent to `u8` in all ways. It is used as a convention to distinguish values from 8-bit unsigned integer values.", + "rune" = "```odin\nrune :: rune\n```\n`rune` is the set of all Unicode code points. It is internally the same as i32 but distinct.", + "f16be" = "```odin\nf16be :: f16be\n```\n`f16be` is the set of all IEEE-754 16-bit floating-point numbers with big endianness.", + "f16le" = "```odin\nf16le :: f16le\n```\n`f16le` is the set of all IEEE-754 16-bit floating-point numbers with little endianness.", + "f32be" = "```odin\nf32be :: f32be\n```\n`f32be` is the set of all IEEE-754 32-bit floating-point numbers with big endianness.", + "f32le" = "```odin\nf32le :: f32le\n```\n`f32le` is the set of all IEEE-754 32-bit floating-point numbers with little endianness.", + "f64be" = "```odin\nf64be :: f64be\n```\n`f64be` is the set of all IEEE-754 64-bit floating-point numbers with big endianness.", + "f64le" = "```odin\nf64le :: f64le\n```\n`f64le` is the set of all IEEE-754 64-bit floating-point numbers with little endianness.", + "i16be" = "```odin\ni16be :: i16be\n```\n`i16be` is the set of all signed 16-bit integers with big endianness. Range -32768 through 32767.", + "i16le" = "```odin\ni16le :: i16le\n```\n`i16le` is the set of all signed 16-bit integers with little endianness. Range -32768 through 32767.", + "i32be" = "```odin\ni32be :: i32be\n```\n`i32be` is the set of all signed 32-bit integers with big endianness. Range -2147483648 through 2147483647.", + "i32le" = "```odin\ni32le :: i32le\n```\n`i32le` is the set of all signed 32-bit integers with little endianness. Range -2147483648 through 2147483647.", + "i64be" = "```odin\ni64be :: i64be\n```\n`i64be` is the set of all signed 64-bit integers with big endianness. Range -9223372036854775808 through 9223372036854775807.", + "i64le" = "```odin\ni64le :: i64le\n```\n`i64le` is the set of all signed 64-bit integers with little endianness. Range -9223372036854775808 through 9223372036854775807.", + "u16be" = "```odin\nu16be :: u16be\n```\n`u16be` is the set of all unsigned 16-bit integers with big endianness. Range 0 through 65535.", + "u16le" = "```odin\nu16le :: u16le\n```\n`u16le` is the set of all unsigned 16-bit integers with little endianness. Range 0 through 65535.", + "u32be" = "```odin\nu32be :: u32be\n```\n`u32be` is the set of all unsigned 32-bit integers with big endianness. Range 0 through 4294967295.", + "u32le" = "```odin\nu32le :: u32le\n```\n`u32le` is the set of all unsigned 32-bit integers with little endianness. Range 0 through 4294967295.", + "u64be" = "```odin\nu64be :: u64be\n```\n`u64be` is the set of all unsigned 64-bit integers with big endianness. Range 0 through 18446744073709551615.", + "u64le" = "```odin\nu64le :: u64le\n```\n`u64le` is the set of all unsigned 64-bit integers with little endianness. Range 0 through 18446744073709551615.", + "i128be" = "```odin\ni128be :: i128be\n```\n`i128be` is the set of all signed 128-bit integers with big endianness. Range -170141183460469231731687303715884105728 through 170141183460469231731687303715884105727.", + "i128le" = "```odin\ni128le :: i128le\n```\n`i128le` is the set of all signed 128-bit integers with little endianness. Range -170141183460469231731687303715884105728 through 170141183460469231731687303715884105727.", + "u128be" = "```odin\nu128be :: u128be\n```\n`u128be` is the set of all unsigned 128-bit integers with big endianness. Range 0 through 340282366920938463463374607431768211455.", + "u128le" = "```odin\nu128le :: u128le\n```\n`u128le` is the set of all unsigned 128-bit integers with little endianness. Range 0 through 340282366920938463463374607431768211455.", + "complex32" = "```odin\ncomplex32 :: complex32\n```\n`complex32` is the set of all complex numbers with `f16` real and imaginary parts.", + "complex64" = "```odin\ncomplex64 :: complex64\n```\n`complex64` is the set of all complex numbers with `f32` real and imaginary parts.", + "complex128" = "```odin\ncomplex128 :: complex128\n```\n`complex128` is the set of all complex numbers with `f64` real and imaginary parts.", + "quaternion64" = "```odin\nquaternion64 :: quaternion64\n```\n`quaternion64` is the set of all complex numbers with `f16` real and imaginary (i, j, & k) parts.", + "quaternion128" = "```odin\nquaternion128 :: quaternion128\n```\n`quaternion128` is the set of all complex numbers with `f32` real and imaginary (i, j, & k) parts.", + "quaternion256" = "```odin\nquaternion256 :: quaternion256\n```\n`quaternion256` is the set of all complex numbers with `f64` real and imaginary (i, j, & k) parts.", + "uintptr" = "```odin\nuintptr :: uintptr\n```\n`uintptr` is an unsigned integer type that is large enough to hold the bit pattern of any pointer.", + "rawptr" = "```odin\nrawptr :: rawptr\n```\n`rawptr` is a pointer to an arbitrary type. It is equivalent to void * in C.", + // taken from https://github.com/odin-lang/Odin/wiki/Keywords-and-Operators + "asm" = "", + "auto_cast" = "```odin\nauto_cast v```\nAutomatically casts an expression `v` to the destination’s type if possible.", + "bit_field" = "", + "bit_set" = "", + "break" = "", + "case" = "", + "cast" = "```odin\ncast(T)v\n```\nConverts the value `v` to the type `T`.", + "const" = "", + "context" = "```odin\nruntime.context: Context\n```\nThe context variable is local to each scope. It is copy-on-write and is implicitly passed by pointer to any procedure call in that scope (if the procedure has the Odin calling convention).", + "continue" = "", + "defer" = "", + "distinct" = "```odin\ndistinct T\n```\nCreate a new type with the same underlying semantics as `T`", + "do" = "", + "dynamic" = "", + "else" = "", + "enum" = "", + "fallthrough" = "", + "for" = "", + "foreign" = "", + "if" = "", + "import" = "", + "in" = "", + "inline" = "", + "map" = "", + "not_in" = "", + "or_break" = "", + "or_continue" = "", + "or_else" = "", + "or_return" = "", + "opaque" = "", + "package" = "", + "proc" = "", + "return" = "", + "struct" = "", + "switch" = "", + "transmute" = "```odin\ntransmute(T)v\n```\nBitwise cast between 2 types of the same size.", + "typeid" = "", + "union" = "", + "using" = "", + "when" = "", + "where" = "", +} + +directive_docs : map[string]string = { + // Record memory layout + "packed" = "```odin\n#packed\n```\n\nThis tag can be applied to a struct. Removes padding between fields that’s normally inserted to ensure all fields meet their type’s alignment requirements. Fields remain in source order.\n\nThis is useful where the structure is unlikely to be correctly aligned (the insertion rules for padding assume it is), or if the space-savings are more important or useful than the access speed of the fields.\n\nAccessing a field in a packed struct may require copying the field out of the struct into a temporary location, or using a machine instruction that doesn’t assume the pointer address is correctly aligned, in order to be performant or avoid crashing on some systems. (See `intrinsics.unaligned_load`.)", + "raw_union" = "```odin\n#raw_union\n```\n\nThis tag can be applied to a `struct`. Struct’s fields will share the same memory space which serves the same functionality as `union`s in C language. Useful when writing bindings especially.", + "align" = "```odin\n#align\n```\n\nThis tag can be applied to a `struct` or `union`. When `#align` is passed an integer `N` (as in `#align N`), it specifies that the `struct` will be aligned to `N` bytes. The `struct`’s fields will remain in source-order.", + "no_nil" = "```odin\n#align\n```\n\nThis tag can be applied to a union to not allow `nil` values.", + // Control statements + "partial" = "```odin\n#partial\n```\n\nBy default all `case`s of an `enum` or `union` have to be covered in a `switch` statement. The reason for this requirement is because it makes accidental bugs less likely. However, the `#partial` tag allows you to not have to write out cases that you don’t need to handle.\n\nThe `#partial` directive can also be used to initialize an enumerated array.", + // Procedure parameters + "no_alias" = "```odin\n#no_alias\n```\n\nThis tag can be applied to a procedure parameter that is a pointer. This is a hint to the compiler that this parameter will not alias other parameters. This is equivalent to C’s `__restrict`.", + "any_int" = "```odin\n#any_int\n```\n\n`#any_int` enables implicit casts to a procedure’s integer type at the call site. A parameter with `#any_int` must be an integer.", + "caller_location" = "```odin\n#caller_location\n```\n\n`#caller_location` sets a parameter’s default value to the location of the code calling the procedure. The location value has the type `runtime.Source_Code_Location`. `#caller_location` may only be used as a default value for procedure parameters.", + "caller_expression" = "```odin\n#caller_expression\n// or\n#caller_expression(<count>)\n```\n\n`#caller_expression` gives a procedure the entire call expression or the expression used to create a parameter. `#caller_expression` may only be used as a default value for procedure parameters.", + "c_vararg" = "```odin\n#c_vararg\n```\n\nUsed to interface with vararg functions in foreign procedures.", + "by_ptr" = "```odin\n#by_ptr\n```\n\nUsed to interface with const reference parameters in foreign procedures. The parameter is passed by pointer internally.", + "optional_ok" = "```odin\n#optional_ok\n```\n\nAllows skipping the last return parameter, which needs to be a `bool`.", + "optional_allocator_error" = "```odin\n#optional_allocator_error\n```\n\nAllows skipping the last return parameter, which needs to be a runtime.Allocator_Error", + // Expressions + "type" = "```odin\n#type\n```\n\nThis tag doesn’t serve a functional purpose in the compiler, this is for telling someone reading the code that the expression is a type. The main case is for showing that a procedure signature without a body is a type and not just missing its body.", + "sparse" = "```odin\n#sparse\n```\n\nThis directive may be used to create a sparse enumerated array. This is necessary when the enumerated values are not contiguous.", + "force_inline" = "```odin\n#force_inline\n```\n\nSpecify whether a procedure literal or call will be forced to inline (`#force_inline`) or forced to never inline `#force_no_inline`. This is not an suggestion to the compiler. If the compiler cannot inline the procedure, it will (currently) silently ignore the directive.\n\nThis is enabled all optization levels except `-o:none` which has all inlining disabled.", + "force_no_inline" = "```odin\n#force_no_inline\n```\n\nSpecify whether a procedure literal or call will be forced to inline (`#force_inline`) or forced to never inline `#force_no_inline`. This is not an suggestion to the compiler. If the compiler cannot inline the procedure, it will (currently) silently ignore the directive.\n\nThis is enabled all optization levels except `-o:none` which has all inlining disabled.", + // Statements + "bounds_check" = "```odin\n#bounds_check\n```\n\nThe `#bounds_check` and `#no_bounds_check` flags control Odin’s built-in bounds checking of arrays and slices. Any statement, block, or function with one of these flags will have their bounds checking turned on or off, depending on the flag provided.\n\nBy default, the Odin compiler has bounds checking enabled program-wide where applicable, and it may be turned off by passing the -no-bounds-check build flag.", + "no_bounds_check" = "```odin\n#no_bounds_check\n```\n\nThe `#bounds_check` and `#no_bounds_check` flags control Odin’s built-in bounds checking of arrays and slices. Any statement, block, or function with one of these flags will have their bounds checking turned on or off, depending on the flag provided.\n\nBy default, the Odin compiler has bounds checking enabled program-wide where applicable, and it may be turned off by passing the -no-bounds-check build flag.", + "type_assert" = "```odin\n#type_assert\n```\n\n`#no_type_assert` will bypass the underlying call to `runtime.type_assertion_check` when placed at the head of a statement or block which would normally do a type assert, such as the resolution of a `union` or an `any` into its true type. `#type_assert` will re-enable type assertions, if they were turned off in an outer scope.\n\nBy default, the Odin compiler has type assertions enabled program-wide where applicable, and they may be turned off by passing the `-no-type-assert` build flag. Note that `-disable-assert` does not also turn off type assertions; `-no-type-assert` must be passed explicitly.", + "no_type_assert" = "```odin\n#no_type_assert\n```\n\n`#no_type_assert` will bypass the underlying call to `runtime.type_assertion_check` when placed at the head of a statement or block which would normally do a type assert, such as the resolution of a `union` or an `any` into its true type. `#type_assert` will re-enable type assertions, if they were turned off in an outer scope.\n\nBy default, the Odin compiler has type assertions enabled program-wide where applicable, and they may be turned off by passing the `-no-type-assert` build flag. Note that `-disable-assert` does not also turn off type assertions; `-no-type-assert` must be passed explicitly.", + // Built-in + "assert" = "```odin\n#assert\n```\n\nUnlike `assert`, `#assert` runs at compile-time. `#assert` breaks compilation if the given bool expression is false, and thus #assert is useful for catching bugs before they ever even reach run-time. It also has no run-time cost.", + "panic" = "```odin\n#panic(<string>)\n```\n\nPanic runs at compile-time. It is functionally equivalent to an `#assert` with a `false` condition, but `#panic` has an error message string parameter.", + "config" = "```odin\n#config(<identifier>, default)\n```\n\nChecks if an identifier is defined through the command line, or gives a default value instead.\n\nValues can be set with the `-define:NAME=VALUE` command line flag.", + "defined" = "```odin\n#defined\n```\n\nChecks if an identifier is defined. This may only be used within a procedure’s body.", + "file" = "```odin\n#file\n```\n\nReturn the current file path.", + "directory" = "```odin\n#directory\n```\n\nReturn the current directory.", + "line" = "```odin\n#line\n```\n\nReturn the current line number.", + "procedure" = "```odin\n#procedure\n```\n\nReturn the current procedure name.", + "exists" = "```odin\n#exists(<string-path>)\n```\n\nReturns `true` or `false` if the file at the given path exists. If the path is relative, it is accessed relative to the Odin source file that references it.", + "branch_location" = "```odin\n#branch_location\n```\n\nWhen used within a `defer` statement, this directive returns a `runtime.Source_Code_Location` of the point at which the control flow triggered execution of the `defer`. This may be a `return` statement or the end of a scope.", + "location" = "```odin\n#location()\n// or\n#location(<entity>)\n```\n\nReturns a `runtime.Source_Code_Location`. Can be called with no parameters for current location, or with a parameter for the location of the variable/proc declaration.", + "load" = "```odin\n#load(<string-path>)\n//or\n#load(<string-path>, <type>)\n```\n\nReturns a `[]u8` of the file contents at compile time. This means that the loaded data is baked into your program. Optionally, you can provide a type name as second argument; interpreting the data as being of that type.\n\n`#load` also works with `or_else` to provide default content when the file wasn’t found", + "hash" = "```odin\n#hash(<string-text>, <string-hash>)\n```\n\nReturns a constant integer of the hash of a string literal at compile time.\n\nAvailable hashes:\n\n- adler32\n- crc32\n- crc64\n- fnv32\n- fnv64\n- fnv32a\n- fnv64a\n- murmur32\n- murmur64", + "load_hash" = "```odin\n#load_hash(<string-path>, <string-hash>)\n```\n\nReturns a constant integer of the hash of a file’s contents at compile time..\n\nAvailable hashes:\n\n- adler32\n- crc32\n- crc64\n- fnv32\n- fnv64\n- fnv32a\n- fnv64a\n- murmur32\n- murmur64", + "load_directory" = "```odin\n#load_directory(<string-path>)\n```\n\nLoads all files within a directory, at compile time. All the data of those files will be baked into your program. Returns `[]runtime.Load_Directory_File`.", +} diff --git a/src/server/documents.odin b/src/server/documents.odin index ed1fb52..3ac0263 100644 --- a/src/server/documents.odin +++ b/src/server/documents.odin @@ -4,12 +4,10 @@ import "base:intrinsics" import "core:fmt" import "core:log" -import "core:mem" import "core:mem/virtual" import "core:odin/ast" import "core:odin/parser" import "core:odin/tokenizer" -import "core:os" import "core:path/filepath" import path "core:path/slashpath" import "core:strings" @@ -166,7 +164,7 @@ document_setup :: proc(document: ^Document) { //Right now not all clients return the case correct windows path, and that causes issues with indexing, so we ensure that it's case correct. when ODIN_OS == .Windows { package_name := path.dir(document.uri.path, context.temp_allocator) - forward, _ := filepath.to_slash(common.get_case_sensitive_path(package_name), context.temp_allocator) + forward, _ := filepath.replace_path_separators(common.get_case_sensitive_path(package_name), '/', context.temp_allocator) if forward == "" { document.package_name = package_name } else { @@ -181,9 +179,9 @@ document_setup :: proc(document: ^Document) { fullpath: string if correct == "" { //This is basically here to handle the tests where the physical file doesn't actual exist. - document.fullpath, _ = filepath.to_slash(document.uri.path) + document.fullpath, _ = filepath.replace_path_separators(document.uri.path, '/', context.temp_allocator) } else { - document.fullpath, _ = filepath.to_slash(correct) + document.fullpath, _ = filepath.replace_path_separators(correct, '/', context.temp_allocator) } } else { document.fullpath = document.uri.path diff --git a/src/server/file_resolve.odin b/src/server/file_resolve.odin index 1b7a5c5..c4b2467 100644 --- a/src/server/file_resolve.odin +++ b/src/server/file_resolve.odin @@ -1,3 +1,4 @@ +#+feature using-stmt package server import "core:odin/ast" @@ -50,15 +51,11 @@ resolve_ranged_file :: proc( margin := 20 for decl in document.ast.decls { - if _, is_value := decl.derived.(^ast.Value_Decl); !is_value { - continue - } - //Look for declarations that overlap with range if range.start.line - margin <= decl.end.line && decl.pos.line <= range.end.line + margin { resolve_decl(&position_context, &ast_context, document, decl, &symbols, .None, allocator) clear(&ast_context.locals) - } + } } return symbols @@ -87,10 +84,6 @@ resolve_entire_file :: proc( symbols := make(map[uintptr]SymbolAndNode, 10000, allocator) for decl in document.ast.decls { - if _, is_value := decl.derived.(^ast.Value_Decl); !is_value { - continue - } - resolve_decl(&position_context, &ast_context, document, decl, &symbols, flag, allocator) clear(&ast_context.locals) } @@ -463,15 +456,19 @@ resolve_node :: proc(node: ^ast.Node, data: ^FileResolveData) { data.position_context.value_decl = n reset_position_context(data.position_context) + resolve_nodes(n.attributes[:], data) resolve_nodes(n.names, data) resolve_node(n.type, data) resolve_nodes(n.values, data) case ^Package_Decl: case ^Import_Decl: + resolve_nodes(n.attributes[:], data) case ^Foreign_Block_Decl: + resolve_nodes(n.attributes[:], data) resolve_node(n.foreign_library, data) resolve_node(n.body, data) case ^Foreign_Import_Decl: + resolve_nodes(n.attributes[:], data) resolve_node(n.name, data) case ^Proc_Group: resolve_nodes(n.args, data) @@ -508,6 +505,9 @@ resolve_node :: proc(node: ^ast.Node, data: ^FileResolveData) { data.position_context.struct_type = n resolve_node(n.poly_params, data) resolve_node(n.align, data) + for clause in n.where_clauses { + resolve_node(clause, data) + } resolve_node(n.fields, data) if data.flag != .None { diff --git a/src/server/format.odin b/src/server/format.odin index 9f30c49..6a95bed 100644 --- a/src/server/format.odin +++ b/src/server/format.odin @@ -5,8 +5,6 @@ import "src:common" import "src:odin/format" import "src:odin/printer" -import "core:log" - FormattingOptions :: struct { tabSize: uint, insertSpaces: bool, //tabs or spaces diff --git a/src/server/generics.odin b/src/server/generics.odin index db858cc..347f484 100644 --- a/src/server/generics.odin +++ b/src/server/generics.odin @@ -69,42 +69,37 @@ resolve_poly :: proc( } } + return resolve_poly_specialization(ast_context, call_node, call_symbol, specialization, poly_map) +} + +resolve_poly_specialization :: proc( + ast_context: ^AstContext, + call_node: ^ast.Expr, + call_symbol: Symbol, + specialization: ^ast.Expr, + poly_map: ^map[string]^ast.Expr, +) -> bool { + if call_node == nil || specialization == nil { + return false + } + #partial switch p in specialization.derived { case ^ast.Matrix_Type: if call_matrix, ok := call_node.derived.(^ast.Matrix_Type); ok { found := false - if poly_type, ok := p.row_count.derived.(^ast.Poly_Type); ok { - if ident, ok := unwrap_ident(poly_type.type); ok { - save_poly_map(ident, call_matrix.row_count, poly_map) - } - if poly_type.specialization != nil { - return resolve_poly(ast_context, call_matrix.row_count, call_symbol, p.row_count, poly_map) - } - found |= true + if expr_contains_poly(p.row_count) { + found |= resolve_poly_expression(ast_context, call_matrix.row_count, p.row_count, poly_map) } - if poly_type, ok := p.column_count.derived.(^ast.Poly_Type); ok { - if ident, ok := unwrap_ident(poly_type.type); ok { - save_poly_map(ident, call_matrix.column_count, poly_map) - } - - if poly_type.specialization != nil { - return resolve_poly(ast_context, call_matrix.column_count, call_symbol, p.column_count, poly_map) - } - found |= true + if expr_contains_poly(p.column_count) { + found |= resolve_poly_expression(ast_context, call_matrix.column_count, p.column_count, poly_map) } - if poly_type, ok := p.elem.derived.(^ast.Poly_Type); ok { - if ident, ok := unwrap_ident(poly_type.type); ok { - save_poly_map(ident, call_matrix.elem, poly_map) - } - - if poly_type.specialization != nil { - return resolve_poly(ast_context, call_matrix.elem, call_symbol, p.elem, poly_map) - } - found |= true + if expr_contains_poly(p.elem) { + found |= resolve_poly_expression(ast_context, call_matrix.elem, p.elem, poly_map) } + return found } case ^ast.Call_Expr: @@ -143,15 +138,8 @@ resolve_poly :: proc( } } - if poly_type, ok := p.elem.derived.(^ast.Poly_Type); ok { - if ident, ok := unwrap_ident(poly_type.type); ok { - save_poly_map(ident, call_array.elem, poly_map) - } - - if poly_type.specialization != nil { - return resolve_poly(ast_context, call_array.elem, call_symbol, p.elem, poly_map) - } - return true + if expr_contains_poly(p.elem) { + return resolve_poly_expression(ast_context, call_array.elem, p.elem, poly_map) } } case ^ast.Array_Type: @@ -172,114 +160,54 @@ resolve_poly :: proc( } } - if poly_type, ok := p.elem.derived.(^ast.Poly_Type); ok { - if ident, ok := unwrap_ident(poly_type.type); ok { - save_poly_map(ident, call_array.elem, poly_map) - } - - if poly_type.specialization != nil { - return resolve_poly(ast_context, call_array.elem, call_symbol, p.elem, poly_map) - } - found |= true + if expr_contains_poly(p.elem) { + found |= resolve_poly_expression(ast_context, call_array.elem, p.elem, poly_map) } - if p.len != nil { - if poly_type, ok := p.len.derived.(^ast.Poly_Type); ok { - if ident, ok := unwrap_ident(poly_type.type); ok { - save_poly_map(ident, call_array.len, poly_map) - } - if poly_type.specialization != nil { - return resolve_poly(ast_context, call_array.len, call_symbol, p.len, poly_map) - } - found |= true - } + if p.len != nil && expr_contains_poly(p.len) { + found |= resolve_poly_expression(ast_context, call_array.len, p.len, poly_map) } return found } case ^ast.Ellipsis: if call_array, ok := call_node.derived.(^ast.Array_Type); ok { - found := false - if array_is_soa(call_array^) { return false } - if poly_type, ok := p.expr.derived.(^ast.Poly_Type); ok { - if ident, ok := unwrap_ident(poly_type.type); ok { - save_poly_map(ident, call_array.elem, poly_map) - } - - if poly_type.specialization != nil { - return resolve_poly(ast_context, call_array.elem, call_symbol, p.expr, poly_map) - } - found |= true + if expr_contains_poly(p.expr) { + return resolve_poly_expression(ast_context, call_array.elem, p.expr, poly_map) } - return found } case ^ast.Map_Type: if call_map, ok := call_node.derived.(^ast.Map_Type); ok { found := false - if poly_type, ok := p.key.derived.(^ast.Poly_Type); ok { - if ident, ok := unwrap_ident(poly_type.type); ok { - save_poly_map(ident, call_map.key, poly_map) - } - - if poly_type.specialization != nil { - return resolve_poly(ast_context, call_map.key, call_symbol, p.key, poly_map) - } - found |= true + if expr_contains_poly(p.key) { + found |= resolve_poly_expression(ast_context, call_map.key, p.key, poly_map) } - if poly_type, ok := p.value.derived.(^ast.Poly_Type); ok { - if ident, ok := unwrap_ident(poly_type.type); ok { - save_poly_map(ident, call_map.value, poly_map) - } - - if poly_type.specialization != nil { - return resolve_poly(ast_context, call_map.value, call_symbol, p.value, poly_map) - } - found |= true + if expr_contains_poly(p.value) { + found |= resolve_poly_expression(ast_context, call_map.value, p.value, poly_map) } return found } case ^ast.Multi_Pointer_Type: if call_pointer, ok := call_node.derived.(^ast.Multi_Pointer_Type); ok { - if poly_type, ok := p.elem.derived.(^ast.Poly_Type); ok { - if ident, ok := unwrap_ident(poly_type.type); ok { - save_poly_map(ident, call_pointer.elem, poly_map) - } - - if poly_type.specialization != nil { - return resolve_poly(ast_context, call_pointer.elem, call_symbol, p.elem, poly_map) - } - return true + if expr_contains_poly(p.elem) { + return resolve_poly_expression(ast_context, call_pointer.elem, p.elem, poly_map) } } case ^ast.Pointer_Type: if call_pointer, ok := call_node.derived.(^ast.Pointer_Type); ok { - if poly_type, ok := p.elem.derived.(^ast.Poly_Type); ok { - if ident, ok := unwrap_ident(poly_type.type); ok { - save_poly_map(ident, call_pointer.elem, poly_map) - } - - if poly_type.specialization != nil { - return resolve_poly(ast_context, call_pointer.elem, call_symbol, p.elem, poly_map) - } - return true + if expr_contains_poly(p.elem) { + return resolve_poly_expression(ast_context, call_pointer.elem, p.elem, poly_map) } } case ^ast.Comp_Lit: if comp_lit, ok := call_node.derived.(^ast.Comp_Lit); ok { - if poly_type, ok := p.type.derived.(^ast.Poly_Type); ok { - if ident, ok := unwrap_ident(poly_type.type); ok { - save_poly_map(ident, comp_lit.type, poly_map) - } - - if poly_type.specialization != nil { - return resolve_poly(ast_context, comp_lit.type, call_symbol, p.type, poly_map) - } - return true + if expr_contains_poly(p.type) { + return resolve_poly_expression(ast_context, comp_lit.type, p.type, poly_map) } } case ^ast.Struct_Type, ^ast.Proc_Type: @@ -292,6 +220,27 @@ resolve_poly :: proc( return false } +resolve_poly_expression :: proc( + ast_context: ^AstContext, + call_node: ^ast.Expr, + poly_node: ^ast.Expr, + poly_map: ^map[string]^ast.Expr, +) -> bool { + if poly_type, ok := poly_node.derived.(^ast.Poly_Type); ok { + if ident, ok := unwrap_ident(poly_type.type); ok { + save_poly_map(ident, call_node, poly_map) + } + + if poly_type.specialization == nil { + return true + } + } + + call_symbol := Symbol{} + internal_resolve_type_expression(ast_context, call_node, &call_symbol) + return resolve_poly(ast_context, call_node, call_symbol, poly_node, poly_map) +} + is_generic_type_recursive :: proc(expr: ^ast.Expr, name: string) -> bool { Data :: struct { name: string, @@ -503,6 +452,18 @@ resolve_generic_function_ast :: proc( return resolve_generic_function_symbol(ast_context, params, results, proc_lit.inlining, proc_symbol) } +get_proc_return_value_count :: proc(fields: []^ast.Field) -> int { + total := 0 + for field in fields { + if len(field.names) == 0 { + total += 1 + } else { + total += len(field.names) + } + } + + return total +} resolve_generic_function_symbol :: proc( ast_context: ^AstContext, @@ -524,6 +485,8 @@ resolve_generic_function_symbol :: proc( i := 0 count_required_params := 0 + // Total number of args passed in the call when expanded to include functions that may return multiple values + call_arg_count := 0 for param in params { if param.default_value == nil { @@ -560,6 +523,7 @@ resolve_generic_function_symbol :: proc( //If we have a function call, we should instead look at the return value: bar(foo(123)) if symbol_value, ok := symbol.value.(SymbolProcedureValue); ok && len(symbol_value.return_types) > 0 { + call_arg_count += get_proc_return_value_count(symbol_value.return_types) if _, ok := call_expr.args[i].derived.(^ast.Call_Expr); ok { if symbol_value.return_types[0].type != nil { if symbol, ok = resolve_type_expression(ast_context, symbol_value.return_types[0].type); @@ -575,6 +539,8 @@ resolve_generic_function_symbol :: proc( } } } + } else { + call_arg_count += 1 } // We set the offset so we can find it as a local if it's based on the type of a local var @@ -600,7 +566,7 @@ resolve_generic_function_symbol :: proc( find_and_replace_poly_type(v, &poly_map) } - if count_required_params > len(call_expr.args) || count_required_params == 0 || len(call_expr.args) == 0 { + if count_required_params > call_arg_count || count_required_params == 0 || call_arg_count == 0 { return {}, false } @@ -793,6 +759,14 @@ resolve_poly_struct :: proc(ast_context: ^AstContext, b: ^SymbolStructValueBuild v.elem = expr case ^ast.Pointer_Type: v.elem = expr + case ^ast.Call_Expr: + for arg, i in v.args { + if call_ident, ok := arg.derived.(^ast.Ident); ok { + if ident.name == call_ident.name { + v.args[i] = expr + } + } + } } } else if data.parent_proc == nil { data.symbol_value_builder.types[data.i] = expr @@ -806,6 +780,8 @@ resolve_poly_struct :: proc(ast_context: ^AstContext, b: ^SymbolStructValueBuild data.parent = node case ^ast.Proc_Type: data.parent_proc = v + case ^ast.Call_Expr: + data.parent = v } return visitor @@ -884,7 +860,7 @@ resolve_poly_union :: proc(ast_context: ^AstContext, poly_params: ^ast.Field_Lis for arg, i in call_expr.args { if ident, ok := arg.derived.(^ast.Ident); ok { if expr, ok := poly_map[ident.name]; ok { - symbol_value.types[i] = expr + call_expr.args[i] = expr } } } diff --git a/src/server/hover.odin b/src/server/hover.odin index 20651e4..991ea1b 100644 --- a/src/server/hover.odin +++ b/src/server/hover.odin @@ -16,7 +16,11 @@ write_hover_content :: proc(ast_context: ^AstContext, symbol: Symbol) -> MarkupC if cat != "" { content.kind = "markdown" - content.value = fmt.tprintf("```odin\n%v\n```%v", cat, doc) + if doc != "" { + content.value = fmt.tprintf(DOC_FMT_MARKDOWN, cat, doc) + } else { + content.value = fmt.tprintf(DOC_FMT_ODIN, cat) + } } else { content.kind = "plaintext" } @@ -51,10 +55,45 @@ get_hover_information :: proc(document: ^Document, position: common.Position) -> get_locals(document.ast, position_context.function, &ast_context, &position_context) } - if position_context.import_stmt != nil { + if position_context.import_stmt != nil && position_in_node(position_context.import_stmt, position_context.position) { + for imp in document.imports { + if imp.original != position_context.import_stmt.fullpath { + continue + } + + symbol := Symbol { + name = imp.base, + type = .Package, + pkg = imp.name, + value = SymbolPackageValue{}, + } + try_build_package(symbol.pkg) + if symbol, ok = resolve_symbol_return(&ast_context, symbol); ok { + hover.range = common.get_token_range(document.ast.pkg_decl, ast_context.file.src) + hover.contents = write_hover_content(&ast_context, symbol) + return hover, true, true + } + } + return {}, false, true } + if document.ast.pkg_decl != nil && position_in_node(document.ast.pkg_decl, position_context.position) { + symbol := Symbol { + name = document.ast.pkg_name, + type = .Package, + pkg = ast_context.document_package, + value = SymbolPackageValue{}, + } + try_build_package(symbol.pkg) + if symbol, ok = resolve_symbol_return(&ast_context, symbol); ok { + hover.range = common.get_token_range(document.ast.pkg_decl, ast_context.file.src) + hover.contents = write_hover_content(&ast_context, symbol) + return hover, true, true + } + + } + if position_context.type_cast != nil && !position_in_node(position_context.type_cast.type, position_context.position) && !position_in_node(position_context.type_cast.expr, position_context.position) { // check that we're actually on the 'cast' word @@ -66,6 +105,15 @@ get_hover_information :: proc(document: ^Document, position: common.Position) -> } } + if position_context.directive != nil && position_in_node(position_context.directive, position_context.position) { + if str, ok := directive_docs[position_context.directive.name]; ok { + hover.contents.kind = "markdown" + hover.contents.value = str + hover.range = common.get_token_range(position_context.directive, ast_context.file.src) + return hover, true, true + } + } + if position_context.identifier != nil { if ident, ok := position_context.identifier.derived.(^ast.Ident); ok { if str, ok := keywords_docs[ident.name]; ok { @@ -323,7 +371,10 @@ get_hover_information :: proc(document: ^Document, position: common.Position) -> } } - if resolved, ok := resolve_symbol_return(&ast_context, lookup(ident.name, selector.pkg, ast_context.fullpath)); ok { + if resolved, ok := resolve_symbol_return( + &ast_context, + lookup(ident.name, selector.pkg, ast_context.fullpath), + ); ok { build_documentation(&ast_context, &resolved, false) resolved.name = ident.name diff --git a/src/server/imports.odin b/src/server/imports.odin index 256295b..16deec5 100644 --- a/src/server/imports.odin +++ b/src/server/imports.odin @@ -1,8 +1,6 @@ package server -import "core:log" import "core:mem" -import "core:odin/ast" import "base:runtime" diff --git a/src/server/indexer.odin b/src/server/indexer.odin index 3ffc3c3..cbf1bba 100644 --- a/src/server/indexer.odin +++ b/src/server/indexer.odin @@ -1,13 +1,8 @@ package server -import "core:fmt" import "core:log" -import "core:odin/ast" -import "core:path/filepath" -import "core:slice" import "core:strings" - Indexer :: struct { builtin_packages: [dynamic]string, runtime_package: string, @@ -47,11 +42,38 @@ should_skip_private_symbol :: proc(symbol: Symbol, current_pkg, current_file: st return false } +is_builtin_pkg :: proc(pkg: string) -> bool { + return strings.equal_fold(pkg, "$builtin") || strings.has_suffix(pkg, "/builtin") +} + +lookup_builtin_symbol :: proc(name: string, current_file: string) -> (Symbol, bool) { + if symbol, ok := lookup_symbol(name, "$builtin", current_file); ok { + return symbol, true + } + + for built in indexer.builtin_packages { + if symbol, ok := lookup_symbol(name, built, current_file); ok { + return symbol, true + } + } + + return {}, false +} + lookup :: proc(name: string, pkg: string, current_file: string, loc := #caller_location) -> (Symbol, bool) { if name == "" { return {}, false } + if is_builtin_pkg(pkg) { + return lookup_builtin_symbol(name, current_file) + } + + return lookup_symbol(name, pkg, current_file) +} + +@(private = "file") +lookup_symbol ::proc(name: string, pkg: string, current_file: string) -> (Symbol, bool) { if symbol, ok := memory_index_lookup(&indexer.index, name, pkg); ok { current_pkg := get_package_from_filepath(current_file) if should_skip_private_symbol(symbol, current_pkg, current_file) { diff --git a/src/server/locals.odin b/src/server/locals.odin index edebd85..e24d27b 100644 --- a/src/server/locals.odin +++ b/src/server/locals.odin @@ -1,3 +1,4 @@ +#+feature using-stmt package server import "core:log" @@ -6,6 +7,7 @@ import "core:odin/ast" LocalFlag :: enum { Mutable, // or constant Variable, // or type + PolyType, } DocumentLocal :: struct { @@ -216,6 +218,8 @@ get_generic_assignment :: proc( } } } + } else if directive, ok := v.expr.derived.(^Basic_Directive); ok { + append(results, v) } //We have to resolve early and can't rely on lazy evalutation because it can have multiple returns. @@ -1064,6 +1068,11 @@ get_locals_proc_param_and_results :: proc( if proc_lit.type != nil && proc_lit.type.params != nil { for arg in proc_lit.type.params.list { for name in arg.names { + flags: bit_set[LocalFlag] = {.Mutable} + if _, ok := name.derived.(^ast.Poly_Type); ok { + flags |= {.PolyType} + } + if arg.type != nil { str := get_ast_node_string(name, file.src) store_local( @@ -1074,7 +1083,7 @@ get_locals_proc_param_and_results :: proc( str, ast_context.non_mutable_only, false, - {.Mutable}, + flags, "", true, ) @@ -1095,7 +1104,7 @@ get_locals_proc_param_and_results :: proc( str, ast_context.non_mutable_only, false, - {.Mutable}, + flags, "", true, ) diff --git a/src/server/memory_index.odin b/src/server/memory_index.odin index 80c137f..487d7a2 100644 --- a/src/server/memory_index.odin +++ b/src/server/memory_index.odin @@ -1,8 +1,6 @@ package server import "core:fmt" -import "core:hash" -import "core:log" import "core:slice" import "core:strings" diff --git a/src/server/methods.odin b/src/server/methods.odin index 757b37d..19d9ff4 100644 --- a/src/server/methods.odin +++ b/src/server/methods.odin @@ -73,7 +73,7 @@ append_method_completion :: proc( for c in cases { method := Method { name = c, - pkg = selector_symbol.pkg, + pkg = "$builtin", // Untyped values are always builtin types } collect_methods( ast_context, @@ -86,9 +86,14 @@ append_method_completion :: proc( ) } } else { + // For typed values, check if it's a builtin type + method_pkg := selector_symbol.pkg + if is_builtin_type_name(selector_symbol.name) { + method_pkg = "$builtin" + } method := Method { name = selector_symbol.name, - pkg = selector_symbol.pkg, + pkg = method_pkg, } collect_methods( ast_context, @@ -114,83 +119,218 @@ collect_methods :: proc( results: ^[dynamic]CompletionResult, ) { for k, v in indexer.index.collection.packages { - if symbols, ok := &v.methods[method]; ok { - for &symbol in symbols { - if should_skip_private_symbol(symbol, ast_context.current_package, ast_context.fullpath) { - continue - } - resolve_unresolved_symbol(ast_context, &symbol) - - range, ok := get_range_from_selection_start_to_dot(position_context) - - if !ok { - return - } - - value: SymbolProcedureValue - value, ok = symbol.value.(SymbolProcedureValue) - - if !ok { - continue - } - - if len(value.arg_types) == 0 || value.arg_types[0].type == nil { - continue - } - - first_arg: Symbol - first_arg, ok = resolve_type_expression(ast_context, value.arg_types[0].type) - - if !ok { - continue - } - - pointers_to_add := first_arg.pointers - pointers - - references := "" - dereferences := "" - - if pointers_to_add > 0 { - for i in 0 ..< pointers_to_add { - references = fmt.tprintf("%v&", references) - } - } else if pointers_to_add < 0 { - for i in pointers_to_add ..< 0 { - dereferences = fmt.tprintf("%v^", dereferences) - } - } - - new_text := "" - - if symbol.pkg != ast_context.document_package { - new_text = fmt.tprintf( - "%v.%v", - path.base(get_symbol_pkg_name(ast_context, &symbol), false, ast_context.allocator), - symbol.name, - ) - } else { - new_text = fmt.tprintf("%v", symbol.name) - } - - if len(symbol.value.(SymbolProcedureValue).arg_types) > 1 { - new_text = fmt.tprintf("%v(%v%v%v$0)", new_text, references, receiver, dereferences) - } else { - new_text = fmt.tprintf("%v(%v%v%v)$0", new_text, references, receiver, dereferences) - } - - item := CompletionItem { - label = symbol.name, - kind = symbol_type_to_completion_kind(symbol.type), - detail = get_short_signature(ast_context, symbol), - additionalTextEdits = remove_edit, - textEdit = TextEdit{newText = new_text, range = {start = range.end, end = range.end}}, - insertTextFormat = .Snippet, - InsertTextMode = .adjustIndentation, - documentation = construct_symbol_docs(symbol), - } - - append(results, CompletionResult{completion_item = item}) + symbols, ok := &v.methods[method] + if !ok { + continue + } + + for &symbol in symbols { + if should_skip_private_symbol(symbol, ast_context.current_package, ast_context.fullpath) { + continue + } + resolve_unresolved_symbol(ast_context, &symbol) + + #partial switch &sym_value in symbol.value { + case SymbolProcedureValue: + add_proc_method_completion( + ast_context, + position_context, + &symbol, + sym_value, + pointers, + receiver, + remove_edit, + results, + ) + case SymbolProcedureGroupValue: + add_proc_group_method_completion( + ast_context, + position_context, + &symbol, + sym_value, + pointers, + receiver, + remove_edit, + results, + ) + } + } + } +} + +@(private = "file") +add_proc_method_completion :: proc( + ast_context: ^AstContext, + position_context: ^DocumentPositionContext, + symbol: ^Symbol, + value: SymbolProcedureValue, + pointers: int, + receiver: string, + remove_edit: []TextEdit, + results: ^[dynamic]CompletionResult, +) { + if len(value.arg_types) == 0 || value.arg_types[0].type == nil { + return + } + + range, ok := get_range_from_selection_start_to_dot(position_context) + if !ok { + return + } + + first_arg: Symbol + first_arg, ok = resolve_type_expression(ast_context, value.arg_types[0].type) + if !ok { + return + } + + references, dereferences := compute_pointer_adjustments(first_arg.pointers, pointers) + + new_text := build_method_call_text( + ast_context, + symbol, + receiver, + references, + dereferences, + len(value.arg_types) > 1, + ) + + item := CompletionItem { + label = symbol.name, + kind = symbol_type_to_completion_kind(symbol.type), + detail = get_short_signature(ast_context, symbol^), + additionalTextEdits = remove_edit, + textEdit = TextEdit{newText = new_text, range = {start = range.end, end = range.end}}, + insertTextFormat = .Snippet, + InsertTextMode = .adjustIndentation, + documentation = construct_symbol_docs(symbol^), + } + + append(results, CompletionResult{completion_item = item}) +} + +@(private = "file") +add_proc_group_method_completion :: proc( + ast_context: ^AstContext, + position_context: ^DocumentPositionContext, + symbol: ^Symbol, + value: SymbolProcedureGroupValue, + pointers: int, + receiver: string, + remove_edit: []TextEdit, + results: ^[dynamic]CompletionResult, +) { + proc_group, is_group := value.group.derived.(^ast.Proc_Group) + if !is_group || len(proc_group.args) == 0 { + return + } + + range, ok := get_range_from_selection_start_to_dot(position_context) + if !ok { + return + } + + // Get first member to determine pointer adjustments + first_member: Symbol + first_member, ok = resolve_type_expression(ast_context, proc_group.args[0]) + if !ok { + return + } + + member_proc, is_proc := first_member.value.(SymbolProcedureValue) + if !is_proc || len(member_proc.arg_types) == 0 || member_proc.arg_types[0].type == nil { + return + } + + first_arg: Symbol + first_arg, ok = resolve_type_expression(ast_context, member_proc.arg_types[0].type) + if !ok { + return + } + + references, dereferences := compute_pointer_adjustments(first_arg.pointers, pointers) + + // Check if any member of the proc group has additional arguments beyond the receiver + has_additional_args := false + for member_expr in proc_group.args { + member: Symbol + member, ok = resolve_type_expression(ast_context, member_expr) + if !ok { + continue + } + if proc_val, is_proc_val := member.value.(SymbolProcedureValue); is_proc_val { + if len(proc_val.arg_types) > 1 { + has_additional_args = true + break } } } + + new_text := build_method_call_text(ast_context, symbol, receiver, references, dereferences, has_additional_args) + + item := CompletionItem { + label = symbol.name, + kind = symbol_type_to_completion_kind(symbol.type), + detail = get_short_signature(ast_context, symbol^), + additionalTextEdits = remove_edit, + textEdit = TextEdit{newText = new_text, range = {start = range.end, end = range.end}}, + insertTextFormat = .Snippet, + InsertTextMode = .adjustIndentation, + documentation = construct_symbol_docs(symbol^), + } + + append(results, CompletionResult{completion_item = item}) +} + +@(private = "file") +compute_pointer_adjustments :: proc( + first_arg_pointers: int, + current_pointers: int, +) -> ( + references: string, + dereferences: string, +) { + pointers_to_add := first_arg_pointers - current_pointers + + if pointers_to_add > 0 { + for _ in 0 ..< pointers_to_add { + references = fmt.tprintf("%v&", references) + } + } else if pointers_to_add < 0 { + for _ in pointers_to_add ..< 0 { + dereferences = fmt.tprintf("%v^", dereferences) + } + } + + return references, dereferences +} + +@(private = "file") +build_method_call_text :: proc( + ast_context: ^AstContext, + symbol: ^Symbol, + receiver: string, + references: string, + dereferences: string, + has_additional_args: bool, +) -> string { + new_text: string + + if symbol.pkg != ast_context.document_package { + new_text = fmt.tprintf( + "%v.%v", + path.base(get_symbol_pkg_name(ast_context, symbol), false, ast_context.allocator), + symbol.name, + ) + } else { + new_text = fmt.tprintf("%v", symbol.name) + } + + if has_additional_args { + new_text = fmt.tprintf("%v(%v%v%v$0)", new_text, references, receiver, dereferences) + } else { + new_text = fmt.tprintf("%v(%v%v%v)$0", new_text, references, receiver, dereferences) + } + + return new_text } diff --git a/src/server/position_context.odin b/src/server/position_context.odin index 33de8f3..7687d01 100644 --- a/src/server/position_context.odin +++ b/src/server/position_context.odin @@ -1,10 +1,11 @@ +#+feature using-stmt package server -import "core:strings" import "core:log" import "core:odin/ast" import "core:odin/parser" import "core:odin/tokenizer" +import "core:strings" import "core:unicode/utf8" import "src:common" @@ -61,11 +62,36 @@ DocumentPositionContext :: struct { import_stmt: ^ast.Import_Decl, type_cast: ^ast.Type_Cast, call_commas: []int, + directive: ^ast.Basic_Directive, +} + + +get_stmt_attrs :: proc(decl: ^ast.Stmt) -> []^ast.Attribute { + if decl == nil { + return nil + } + #partial switch v in decl.derived { + case ^ast.Value_Decl: + return v.attributes[:] + case ^ast.Import_Decl: + return v.attributes[:] + case ^ast.Foreign_Block_Decl: + return v.attributes[:] + case ^ast.Foreign_Import_Decl: + return v.attributes[:] + } + return nil } get_document_position_decls :: proc(decls: []^ast.Stmt, position_context: ^DocumentPositionContext) -> bool { exists_in_decl := false for decl in decls { + for attr in get_stmt_attrs(decl) { + if position_in_node(attr, position_context.position) { + get_document_position(attr, position_context) + return true + } + } if position_in_node(decl, position_context.position) { get_document_position(decl, position_context) exists_in_decl = true @@ -820,6 +846,11 @@ get_document_position_node :: proc(node: ^ast.Node, position_context: ^DocumentP position_context.struct_type = n get_document_position(n.poly_params, position_context) get_document_position(n.align, position_context) + for clause in n.where_clauses { + if position_in_node(clause, position_context.position) { + get_document_position(clause, position_context) + } + } get_document_position(n.fields, position_context) case ^Union_Type: position_context.union_type = n @@ -857,6 +888,8 @@ get_document_position_node :: proc(node: ^ast.Node, position_context: ^DocumentP get_document_position(n.name, position_context) get_document_position(n.type, position_context) get_document_position(n.bit_size, position_context) + case ^Basic_Directive: + position_context.directive = n case: } } diff --git a/src/server/references.odin b/src/server/references.odin index ee49c4e..aeebd43 100644 --- a/src/server/references.odin +++ b/src/server/references.odin @@ -16,10 +16,10 @@ import "src:common" fullpaths: [dynamic]string -walk_directories :: proc(info: os.File_Info, in_err: os.Errno, user_data: rawptr) -> (err: os.Error, skip_dir: bool) { +walk_directories :: proc(info: os.File_Info, in_err: os.Error, user_data: rawptr) -> (err: os.Error, skip_dir: bool) { document := cast(^Document)user_data - if info.is_dir { + if info.type == .Directory { return nil, false } @@ -28,7 +28,7 @@ walk_directories :: proc(info: os.File_Info, in_err: os.Errno, user_data: rawptr } if strings.contains(info.name, ".odin") { - slash_path, _ := filepath.to_slash(info.fullpath, context.temp_allocator) + slash_path, _ := filepath.replace_path_separators(info.fullpath, '/', context.temp_allocator) if slash_path != document.fullpath { append(&fullpaths, strings.clone(info.fullpath, context.temp_allocator)) } @@ -277,7 +277,24 @@ resolve_references :: proc( when !ODIN_TEST { for workspace in common.config.workspace_folders { uri, _ := common.parse_uri(workspace.uri, context.temp_allocator) - filepath.walk(uri.path, walk_directories, document) + w := os.walker_create(uri.path) + defer os.walker_destroy(&w) + for info in os.walker_walk(&w) { + if info.type == .Directory { + continue + } + + if info.fullpath == "" { + continue + } + + if strings.contains(info.name, ".odin") { + slash_path, _ := filepath.replace_path_separators(info.fullpath, '/', context.temp_allocator) + if slash_path != document.fullpath { + append(&fullpaths, strings.clone(info.fullpath, context.temp_allocator)) + } + } + } } } @@ -297,12 +314,12 @@ resolve_references :: proc( for fullpath in fullpaths { dir := filepath.dir(fullpath) base := filepath.base(dir) - forward_dir, _ := filepath.to_slash(dir) + forward_dir, _ := filepath.replace_path_separators(dir, '/', context.allocator) - data, ok := os.read_entire_file(fullpath, context.allocator) + data, err := os.read_entire_file(fullpath, context.allocator) - if !ok { - log.errorf("failed to read entire file for indexing %v", fullpath) + if err != nil { + log.errorf("failed to read entire file for indexing %v: %v", fullpath, err) continue } @@ -328,7 +345,7 @@ resolve_references :: proc( pkg = pkg, } - ok = parser.parse_file(&p, &file) + ok := parser.parse_file(&p, &file) if !ok { if !strings.contains(fullpath, "builtin.odin") && !strings.contains(fullpath, "intrinsics.odin") { diff --git a/src/server/requests.odin b/src/server/requests.odin index 82d0ae2..3aec455 100644 --- a/src/server/requests.odin +++ b/src/server/requests.odin @@ -7,9 +7,6 @@ import "base:runtime" import "core:encoding/json" import "core:fmt" import "core:log" -import "core:mem" -import "core:odin/ast" -import "core:odin/parser" import "core:os" import "core:path/filepath" import path "core:path/slashpath" @@ -17,7 +14,6 @@ import "core:slice" import "core:strconv" import "core:strings" import "core:sync" -import "core:thread" import "core:time" import "src:common" @@ -218,7 +214,12 @@ read_and_parse_body :: proc(reader: ^Reader, header: Header) -> (json.Value, boo return value, true } -call_map: map[string]proc(_: json.Value, _: RequestId, _: ^common.Config, _: ^Writer) -> common.Error = { +call_map: map[string]proc( + _: json.Value, + _: RequestId, + _: ^common.Config, + _: ^Writer, +) -> common.Error = { "initialize" = request_initialize, "initialized" = request_initialized, "shutdown" = request_shutdown, @@ -278,6 +279,7 @@ consume_requests :: proc(config: ^common.Config, writer: ^Writer) -> bool { ordered_remove(&requests, delete_index) } } + clear(&deletings) for request in requests { append(&temp_requests, request) @@ -329,7 +331,10 @@ call :: proc(value: json.Value, id: RequestId, writer: ^Writer, config: ^common. if !ok { log.errorf("Failed to find method: %#v", root) - response := make_response_message_error(id = id, error = ResponseError{code = .MethodNotFound, message = ""}) + response := make_response_message_error( + id = id, + error = ResponseError{code = .MethodNotFound, message = ""}, + ) send_error(response, writer) return } @@ -347,7 +352,10 @@ call :: proc(value: json.Value, id: RequestId, writer: ^Writer, config: ^common. } else { err := fn(root["params"], id, config, writer) if err != .None { - response := make_response_message_error(id = id, error = ResponseError{code = err, message = ""}) + response := make_response_message_error( + id = id, + error = ResponseError{code = err, message = ""}, + ) send_error(response, writer) } } @@ -356,29 +364,44 @@ call :: proc(value: json.Value, id: RequestId, writer: ^Writer, config: ^common. //log.errorf("time duration %v for %v", time.duration_milliseconds(diff), method) } -read_ols_initialize_options :: proc(config: ^common.Config, ols_config: OlsConfig, uri: common.Uri) { - config.disable_parser_errors = ols_config.disable_parser_errors.(bool) or_else config.disable_parser_errors +read_ols_initialize_options :: proc( + config: ^common.Config, + ols_config: OlsConfig, + uri: common.Uri, +) { + config.disable_parser_errors = + ols_config.disable_parser_errors.(bool) or_else config.disable_parser_errors config.thread_count = ols_config.thread_pool_count.(int) or_else config.thread_count - config.enable_document_symbols = ols_config.enable_document_symbols.(bool) or_else config.enable_document_symbols + config.enable_document_symbols = + ols_config.enable_document_symbols.(bool) or_else config.enable_document_symbols config.enable_format = ols_config.enable_format.(bool) or_else config.enable_format config.enable_hover = ols_config.enable_hover.(bool) or_else config.enable_hover - config.enable_semantic_tokens = ols_config.enable_semantic_tokens.(bool) or_else config.enable_semantic_tokens - config.enable_unused_imports_reporting = ols_config.enable_unused_imports_reporting.(bool) or_else config.enable_unused_imports_reporting + config.enable_semantic_tokens = + ols_config.enable_semantic_tokens.(bool) or_else config.enable_semantic_tokens + config.enable_unused_imports_reporting = + ols_config.enable_unused_imports_reporting.(bool) or_else config.enable_unused_imports_reporting config.enable_procedure_context = ols_config.enable_procedure_context.(bool) or_else config.enable_procedure_context config.enable_snippets = ols_config.enable_snippets.(bool) or_else config.enable_snippets config.enable_references = ols_config.enable_references.(bool) or_else config.enable_references - config.enable_document_highlights = ols_config.enable_document_highlights.(bool) or_else config.enable_document_highlights + config.enable_document_highlights = + ols_config.enable_document_highlights.(bool) or_else config.enable_document_highlights config.enable_completion_matching = ols_config.enable_completion_matching.(bool) or_else config.enable_completion_matching - config.enable_document_links = ols_config.enable_document_links.(bool) or_else config.enable_document_links + config.enable_document_links = + ols_config.enable_document_links.(bool) or_else config.enable_document_links + config.enable_comp_lit_signature_help = + ols_config.enable_comp_lit_signature_help.(bool) or_else config.enable_comp_lit_signature_help + config.enable_comp_lit_signature_help_use_docs = + ols_config.enable_comp_lit_signature_help_use_docs.(bool) or_else config.enable_comp_lit_signature_help_use_docs config.verbose = ols_config.verbose.(bool) or_else config.verbose config.file_log = ols_config.file_log.(bool) or_else config.file_log config.enable_procedure_snippet = ols_config.enable_procedure_snippet.(bool) or_else config.enable_procedure_snippet - config.enable_auto_import = ols_config.enable_auto_import.(bool) or_else config.enable_auto_import + config.enable_auto_import = + ols_config.enable_auto_import.(bool) or_else config.enable_auto_import config.enable_checker_only_saved = ols_config.enable_checker_only_saved.(bool) or_else config.enable_checker_only_saved @@ -394,7 +417,10 @@ read_ols_initialize_options :: proc(config: ^common.Config, ols_config: OlsConfi } if ols_config.odin_root_override != "" { - config.odin_root_override = strings.clone(ols_config.odin_root_override, context.temp_allocator) + config.odin_root_override = strings.clone( + ols_config.odin_root_override, + context.temp_allocator, + ) allocated: bool config.odin_root_override, allocated = common.resolve_home_dir(config.odin_root_override) @@ -420,6 +446,11 @@ read_ols_initialize_options :: proc(config: ^common.Config, ols_config: OlsConfi } config.profile.os = strings.clone(profile.os) + config.profile.arch = strings.clone(profile.arch) + + for key, value in profile.defines { + config.profile.defines[strings.clone(key)] = strings.clone(value) + } break } @@ -442,7 +473,10 @@ read_ols_initialize_options :: proc(config: ^common.Config, ols_config: OlsConfi config.enable_inlay_hints_implicit_return = ols_config.enable_inlay_hints_implicit_return.(bool) or_else config.enable_inlay_hints_implicit_return - config.enable_fake_method = ols_config.enable_fake_methods.(bool) or_else config.enable_fake_method + config.enable_fake_method = + ols_config.enable_fake_methods.(bool) or_else config.enable_fake_method + config.enable_overload_resolution = + ols_config.enable_overload_resolution.(bool) or_else config.enable_overload_resolution // Delete overriding collections. for it in ols_config.collections { @@ -462,7 +496,7 @@ read_ols_initialize_options :: proc(config: ^common.Config, ols_config: OlsConfi // Apply custom collections. for it in ols_config.collections { - forward_path, _ := filepath.to_slash(it.path, context.temp_allocator) + forward_path, _ := filepath.replace_path_separators(it.path, '/', context.temp_allocator) forward_path = common.resolve_home_dir(forward_path, context.temp_allocator) @@ -470,16 +504,21 @@ read_ols_initialize_options :: proc(config: ^common.Config, ols_config: OlsConfi when ODIN_OS == .Windows { if filepath.is_abs(it.path) { - final_path, _ = filepath.to_slash( + final_path, _ = filepath.replace_path_separators( common.get_case_sensitive_path(forward_path, context.temp_allocator), + '/', context.temp_allocator, ) } else { - final_path, _ = filepath.to_slash( + final_path, _ = filepath.replace_path_separators( common.get_case_sensitive_path( - path.join(elems = {uri.path, forward_path}, allocator = context.temp_allocator), + path.join( + elems = {uri.path, forward_path}, + allocator = context.temp_allocator, + ), context.temp_allocator, ), + '/', context.temp_allocator, ) } @@ -493,13 +532,18 @@ read_ols_initialize_options :: proc(config: ^common.Config, ols_config: OlsConfi } } - if abs_final_path, ok := filepath.abs(final_path); ok { - slashed_path, _ := filepath.to_slash(abs_final_path, context.temp_allocator) + abs_final_path, err := filepath.abs(final_path, context.temp_allocator) + if err != nil { + log.errorf("Failed to find absolute address of collection: %v", final_path, err) + config.collections[strings.clone(it.name)] = strings.clone(final_path) + } else { + slashed_path, _ := filepath.replace_path_separators( + abs_final_path, + '/', + context.temp_allocator, + ) config.collections[strings.clone(it.name)] = strings.clone(slashed_path) - } else { - log.errorf("Failed to find absolute address of collection: %v", final_path) - config.collections[strings.clone(it.name)] = strings.clone(final_path) } } @@ -543,7 +587,8 @@ read_ols_initialize_options :: proc(config: ^common.Config, ols_config: OlsConfi } if odin_core_env != "" { - if abs_core_env, ok := filepath.abs(odin_core_env, context.temp_allocator); ok { + if abs_core_env, err := filepath.abs(odin_core_env, context.temp_allocator); + err == nil { odin_core_env = abs_core_env } } @@ -554,7 +599,11 @@ read_ols_initialize_options :: proc(config: ^common.Config, ols_config: OlsConfi // Insert the default collections if they are not specified in the config. if odin_core_env != "" { - forward_path, _ := filepath.to_slash(odin_core_env, context.temp_allocator) + forward_path, _ := filepath.replace_path_separators( + odin_core_env, + '/', + context.temp_allocator, + ) // base if "base" not_in config.collections { @@ -582,7 +631,10 @@ read_ols_initialize_options :: proc(config: ^common.Config, ols_config: OlsConfi // shared if "shared" not_in config.collections { - shared_path := path.join(elems = {forward_path, "shared"}, allocator = context.allocator) + shared_path := path.join( + elems = {forward_path, "shared"}, + allocator = context.allocator, + ) if os.exists(shared_path) { config.collections[strings.clone("shared")] = shared_path } else { @@ -641,6 +693,7 @@ request_initialize :: proc( config.enable_document_highlights = true config.enable_completion_matching = true config.enable_document_links = true + config.enable_comp_lit_signature_help = false config.verbose = false config.file_log = false config.odin_command = "" @@ -651,21 +704,18 @@ request_initialize :: proc( config.enable_auto_import = true read_ols_config :: proc(file: string, config: ^common.Config, uri: common.Uri) { - if data, ok := os.read_entire_file(file, context.temp_allocator); ok { - if value, err := json.parse(data = data, allocator = context.temp_allocator, parse_integers = true); - err == .None { - ols_config: OlsConfig - - if unmarshal(value, ols_config, context.temp_allocator) == nil { - read_ols_initialize_options(config, ols_config, uri) - } else { - log.warnf("Failed to unmarshal %v", file) - } - } else { - log.warnf("Failed to parse json %v", file) - } + data, err := os.read_entire_file(file, context.temp_allocator) + if err != nil { + log.warnf("Failed to read/find %v: %v", file, err) + return + } + ols_config: OlsConfig + + json_err := json.unmarshal(data, &ols_config, allocator = context.temp_allocator) + if json_err == nil { + read_ols_initialize_options(config, ols_config, uri) } else { - log.warnf("Failed to read/find %v", file) + log.errorf("Failed to unmarshal %v: %v", file, json_err) } } @@ -689,7 +739,10 @@ request_initialize :: proc( read_ols_initialize_options(config, initialize_params.initializationOptions, uri) // Apply ols.json config. - ols_config_path := path.join(elems = {uri.path, "ols.json"}, allocator = context.temp_allocator) + ols_config_path := path.join( + elems = {uri.path, "ols.json"}, + allocator = context.temp_allocator, + ) read_ols_config(ols_config_path, config, uri) } else { read_ols_initialize_options(config, initialize_params.initializationOptions, {}) @@ -710,7 +763,8 @@ request_initialize :: proc( config.enable_label_details = initialize_params.capabilities.textDocument.completion.completionItem.labelDetailsSupport - config.enable_snippets &= initialize_params.capabilities.textDocument.completion.completionItem.snippetSupport + config.enable_snippets &= + initialize_params.capabilities.textDocument.completion.completionItem.snippetSupport config.signature_offset_support = initialize_params.capabilities.textDocument.signatureHelp.signatureInformation.parameterInformation.labelOffsetSupport @@ -719,12 +773,17 @@ request_initialize :: proc( signatureTriggerCharacters := []string{"(", ","} signatureRetriggerCharacters := []string{","} - semantic_range_support := initialize_params.capabilities.textDocument.semanticTokens.requests.range + semantic_range_support := + initialize_params.capabilities.textDocument.semanticTokens.requests.range response := make_response_message( params = ResponseInitializeParams { capabilities = ServerCapabilities { - textDocumentSync = TextDocumentSyncOptions{openClose = true, change = 2, save = {includeText = true}}, + textDocumentSync = TextDocumentSyncOptions { + openClose = true, + change = 2, + save = {includeText = true}, + }, renameProvider = RenameOptions{prepareProvider = true}, workspaceSymbolProvider = true, referencesProvider = config.enable_references, @@ -742,22 +801,23 @@ request_initialize :: proc( }, semanticTokensProvider = SemanticTokensOptions { range = config.enable_semantic_tokens && semantic_range_support, - full = config.enable_semantic_tokens && !semantic_range_support, + full = config.enable_semantic_tokens, legend = SemanticTokensLegend { tokenTypes = semantic_token_type_names, tokenModifiers = semantic_token_modifier_names, }, }, - inlayHintProvider = ( - config.enable_inlay_hints_params || + inlayHintProvider = (config.enable_inlay_hints_params || config.enable_inlay_hints_default_params || - config.enable_inlay_hints_implicit_return - ), + config.enable_inlay_hints_implicit_return), documentSymbolProvider = config.enable_document_symbols, hoverProvider = config.enable_hover, documentFormattingProvider = config.enable_format, documentLinkProvider = {resolveProvider = false}, - codeActionProvider = {resolveProvider = false, codeActionKinds = {"refactor.rewrite"}}, + codeActionProvider = { + resolveProvider = false, + codeActionKinds = {"refactor.rewrite"}, + }, }, }, id = id, @@ -823,7 +883,12 @@ request_initialized :: proc( return .None } -request_shutdown :: proc(params: json.Value, id: RequestId, config: ^common.Config, writer: ^Writer) -> common.Error { +request_shutdown :: proc( + params: json.Value, + id: RequestId, + config: ^common.Config, + writer: ^Writer, +) -> common.Error { response := make_response_message(params = nil, id = id) send_response(response, writer) @@ -855,7 +920,7 @@ request_definition :: proc( return .InternalError } - locations, ok2 := get_definition_location(document, definition_params.position) + locations, ok2 := get_definition_location(document, definition_params.position, config) if !ok2 { log.warn("Failed to get definition location") @@ -938,7 +1003,12 @@ request_completion :: proc( } list: CompletionList - list, ok = get_completion_list(document, completition_params.position, completition_params.context_, config) + list, ok = get_completion_list( + document, + completition_params.position, + completition_params.context_, + config, + ) if !ok { return .InternalError @@ -976,7 +1046,7 @@ request_signature_help :: proc( } help: SignatureHelp - help, ok = get_signature_information(document, signature_params.position) + help, ok = get_signature_information(document, signature_params.position, config) if !ok { return .InternalError @@ -1032,7 +1102,12 @@ request_format_document :: proc( return .None } -notification_exit :: proc(params: json.Value, id: RequestId, config: ^common.Config, writer: ^Writer) -> common.Error { +notification_exit :: proc( + params: json.Value, + id: RequestId, + config: ^common.Config, + writer: ^Writer, +) -> common.Error { config.running = false return .None } @@ -1059,7 +1134,12 @@ notification_did_open :: proc( defer delete(open_params.textDocument.uri) - if n := document_open(open_params.textDocument.uri, open_params.textDocument.text, config, writer); n != .None { + if n := document_open( + open_params.textDocument.uri, + open_params.textDocument.text, + config, + writer, + ); n != .None { return .InternalError } @@ -1158,7 +1238,7 @@ notification_did_save :: proc( when ODIN_OS == .Windows { correct := common.get_case_sensitive_path(fullpath, context.temp_allocator) - fullpath, _ = filepath.to_slash(correct, context.temp_allocator) + fullpath, _ = filepath.replace_path_separators(correct, '/', context.temp_allocator) } corrected_uri := common.create_uri(fullpath, context.temp_allocator) @@ -1295,7 +1375,12 @@ request_document_symbols :: proc( return .None } -request_hover :: proc(params: json.Value, id: RequestId, config: ^common.Config, writer: ^Writer) -> common.Error { +request_hover :: proc( + params: json.Value, + id: RequestId, + config: ^common.Config, + writer: ^Writer, +) -> common.Error { params_object, ok := params.(json.Object) if !ok { @@ -1443,7 +1528,12 @@ request_prepare_rename :: proc( return .None } -request_rename :: proc(params: json.Value, id: RequestId, config: ^common.Config, writer: ^Writer) -> common.Error { +request_rename :: proc( + params: json.Value, + id: RequestId, + config: ^common.Config, + writer: ^Writer, +) -> common.Error { params_object, ok := params.(json.Object) if !ok { @@ -1557,7 +1647,12 @@ request_highlights :: proc( return .None } -request_code_action :: proc(params: json.Value, id: RequestId, config: ^common.Config, writer: ^Writer) -> common.Error { +request_code_action :: proc( + params: json.Value, + id: RequestId, + config: ^common.Config, + writer: ^Writer, +) -> common.Error { params_object, ok := params.(json.Object) if !ok { @@ -1615,7 +1710,7 @@ notification_did_change_watched_files :: proc( find_all_package_aliases() } else { if uri, ok := common.parse_uri(change.uri, context.temp_allocator); ok { - if data, ok := os.read_entire_file(uri.path, context.temp_allocator); ok { + if data, err := os.read_entire_file(uri.path, context.temp_allocator); err == nil { index_file(uri, cast(string)data) } } @@ -1688,6 +1783,11 @@ request_workspace_symbols :: proc( return .None } -request_noop :: proc(params: json.Value, id: RequestId, config: ^common.Config, writer: ^Writer) -> common.Error { +request_noop :: proc( + params: json.Value, + id: RequestId, + config: ^common.Config, + writer: ^Writer, +) -> common.Error { return .None } diff --git a/src/server/semantic_tokens.odin b/src/server/semantic_tokens.odin index d237109..5ff822b 100644 --- a/src/server/semantic_tokens.odin +++ b/src/server/semantic_tokens.odin @@ -5,6 +5,7 @@ https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/spe */ +#+feature using-stmt package server import "core:fmt" @@ -57,12 +58,13 @@ semantic_token_type_names: []string = { SemanticTokenModifier :: enum u8 { Declaration, + DefaultLibrary, Definition, Deprecated, ReadOnly, } // Need to be in the same order as SemanticTokenModifier -semantic_token_modifier_names: []string = {"declaration", "definition", "deprecated", "readonly"} +semantic_token_modifier_names: []string = {"declaration", "defaultLibrary", "definition", "deprecated", "readonly"} SemanticTokenModifiers :: bit_set[SemanticTokenModifier;u32] SemanticTokensRequest :: struct { @@ -493,7 +495,7 @@ visit_bit_field_fields :: proc(node: ast.Bit_Field_Type, builder: ^SemanticToken visit_import_decl :: proc(decl: ^ast.Import_Decl, builder: ^SemanticTokenBuilder) { /* hightlight the namespace in the import declaration - + import "pkg" ^^^ import "core:fmt" @@ -546,6 +548,10 @@ visit_ident :: proc( modifiers := modifiers + if .Builtin in symbol.flags { + modifiers += {.DefaultLibrary} + } + if .Mutable not_in symbol.flags { modifiers += {.ReadOnly} } diff --git a/src/server/signature.odin b/src/server/signature.odin index 51f919d..a53c084 100644 --- a/src/server/signature.odin +++ b/src/server/signature.odin @@ -1,5 +1,6 @@ package server +import "core:fmt" import "core:log" import "core:odin/ast" import "core:odin/tokenizer" @@ -30,7 +31,7 @@ SignatureHelp :: struct { SignatureInformation :: struct { label: string, - documentation: string, + documentation: MarkupContent, parameters: []ParameterInformation, } @@ -62,7 +63,14 @@ seperate_proc_field_arguments :: proc(procedure: ^Symbol) { } -get_signature_information :: proc(document: ^Document, position: common.Position) -> (SignatureHelp, bool) { +get_signature_information :: proc( + document: ^Document, + position: common.Position, + config: ^common.Config, +) -> ( + SignatureHelp, + bool, +) { signature_help: SignatureHelp ast_context := make_ast_context( @@ -81,7 +89,7 @@ get_signature_information :: proc(document: ^Document, position: common.Position ast_context.position_hint = position_context.hint //TODO(should probably not be an ast.Expr, but ast.Call_Expr) - if position_context.call == nil { + if position_context.call == nil && !config.enable_comp_lit_signature_help { return signature_help, true } @@ -90,37 +98,80 @@ get_signature_information :: proc(document: ^Document, position: common.Position if position_context.function != nil { get_locals(document.ast, position_context.function, &ast_context, &position_context) } + signature_information := make([dynamic]SignatureInformation, context.temp_allocator) + + if position_context.call != nil { + signature_help.activeParameter = add_proc_signature(&ast_context, &position_context, &signature_information) + } + + if config.enable_comp_lit_signature_help { + if symbol, ok := resolve_comp_literal(&ast_context, &position_context); ok { + if config.enable_comp_lit_signature_help_use_docs { + build_documentation(&ast_context, &symbol, short_signature = true) + signature := get_signature(symbol) + build_documentation(&ast_context, &symbol, short_signature = false) + append( + &signature_information, + SignatureInformation{label = signature, documentation = write_hover_content(&ast_context, symbol)}, + ) + } else { + build_documentation(&ast_context, &symbol, short_signature = false) + append( + &signature_information, + SignatureInformation{label = get_signature(symbol), documentation = write_markdown_doc(symbol)}, + ) + } + } + } + + signature_help.signatures = signature_information[:] + + return signature_help, true +} + +@(private = "file") +get_signature :: proc(symbol: Symbol) -> string { + sb := strings.builder_make() + write_symbol_name(&sb, symbol) + strings.write_string(&sb, " :: ") + strings.write_string(&sb, symbol.signature) + return strings.to_string(sb) +} +@(private = "file") +add_proc_signature :: proc( + ast_context: ^AstContext, + position_context: ^DocumentPositionContext, + signature_information: ^[dynamic]SignatureInformation, +) -> ( + active_parameter: int, +) { for comma, i in position_context.call_commas { if position_context.position > comma { - signature_help.activeParameter = i + 1 + active_parameter = i + 1 } else if position_context.position == comma { - signature_help.activeParameter = i + active_parameter = i } } if position_context.arrow { - signature_help.activeParameter += 1 + active_parameter = 1 } - call: Symbol - call, ok = resolve_type_expression(&ast_context, position_context.call) - + call, ok := resolve_type_expression(ast_context, position_context.call) if !ok { - return signature_help, true + return active_parameter } seperate_proc_field_arguments(&call) - signature_information := make([dynamic]SignatureInformation, context.temp_allocator) - if value, ok := call.value.(SymbolProcedureValue); ok { parameters := make([]ParameterInformation, len(value.orig_arg_types), context.temp_allocator) for arg, i in value.orig_arg_types { if arg.type != nil { if _, is_ellipsis := arg.type.derived.(^ast.Ellipsis); is_ellipsis { - signature_help.activeParameter = min(i, signature_help.activeParameter) + active_parameter = min(i, active_parameter) } } @@ -130,13 +181,13 @@ get_signature_information :: proc(document: ^Document, position: common.Position sb := strings.builder_make(context.temp_allocator) write_procedure_symbol_signature(&sb, value, detailed_signature = false) call.signature = strings.to_string(sb) - + info := SignatureInformation { - label = get_signature(call), - documentation = construct_symbol_docs(call, markdown = false), + label = get_signature(call), + documentation = write_markdown_doc(call), parameters = parameters, } - append(&signature_information, info) + append(signature_information, info) } else if value, ok := call.value.(SymbolAggregateValue); ok { //function overloaded procedures for symbol in value.symbols { @@ -148,7 +199,7 @@ get_signature_information :: proc(document: ^Document, position: common.Position for arg, i in value.orig_arg_types { if arg.type != nil { if _, is_ellipsis := arg.type.derived.(^ast.Ellipsis); is_ellipsis { - signature_help.activeParameter = min(i, signature_help.activeParameter) + active_parameter = min(i, active_parameter) } } @@ -158,28 +209,22 @@ get_signature_information :: proc(document: ^Document, position: common.Position sb := strings.builder_make(context.temp_allocator) write_procedure_symbol_signature(&sb, value, detailed_signature = false) symbol.signature = strings.to_string(sb) - + info := SignatureInformation { - label = get_signature(symbol), - documentation = construct_symbol_docs(symbol, markdown = false), + label = get_signature(symbol), + documentation = write_markdown_doc(symbol), parameters = parameters, } - append(&signature_information, info) + append(signature_information, info) } } } - - signature_help.signatures = signature_information[:] - - return signature_help, true + return active_parameter } -@(private="file") -get_signature :: proc(symbol: Symbol) -> string { - sb := strings.builder_make() - write_symbol_name(&sb, symbol) - strings.write_string(&sb, " :: ") - strings.write_string(&sb, symbol.signature) - return strings.to_string(sb) +@(private = "file") +write_markdown_doc :: proc(symbol: Symbol) -> MarkupContent { + doc := construct_symbol_docs(symbol) + return MarkupContent{kind = "markdown", value = fmt.tprintf(DOC_FMT_ODIN, doc)} } diff --git a/src/server/symbol.odin b/src/server/symbol.odin index 2869e5c..14e231c 100644 --- a/src/server/symbol.odin +++ b/src/server/symbol.odin @@ -197,6 +197,7 @@ SymbolValue :: union { } SymbolFlag :: enum { + Builtin, Distinct, Deprecated, PrivateFile, @@ -211,6 +212,7 @@ SymbolFlag :: enum { SoaPointer, Simd, Parameter, //If the symbol is a procedure argument + PolyType, } SymbolFlags :: bit_set[SymbolFlag] @@ -412,6 +414,7 @@ write_struct_type :: proc( } } + s: Symbol if _, ok := get_attribute_objc_class_name(attributes); ok { b.symbol.flags |= {.ObjC} if get_attribute_objc_is_class_method(attributes) { @@ -625,6 +628,15 @@ expand_objc :: proc(ast_context: ^AstContext, b: ^SymbolStructValueBuilder) { } } +is_struct_field_using :: proc(v: SymbolStructValue, index: int) -> bool { + for i in v.usings { + if i == index { + return true + } + } + return false +} + get_proc_arg_count :: proc(v: SymbolProcedureValue) -> int { total := 0 for proc_arg in v.arg_types { @@ -911,8 +923,8 @@ construct_struct_field_symbol :: proc(symbol: ^Symbol, parent_name: string, valu symbol.name = value.names[index] symbol.type = .Field symbol.parent_name = parent_name - symbol.doc = get_doc(value.docs[index], context.temp_allocator) - symbol.comment = get_comment(value.comments[index]) + symbol.doc = get_comment(value.docs[index], context.temp_allocator) + symbol.comment = get_comment(value.comments[index], context.temp_allocator) symbol.range = value.ranges[index] } @@ -925,16 +937,16 @@ construct_bit_field_field_symbol :: proc( symbol.name = value.names[index] symbol.parent_name = parent_name symbol.type = .Field - symbol.doc = get_doc(value.docs[index], context.temp_allocator) - symbol.comment = get_comment(value.comments[index]) + symbol.doc = get_comment(value.docs[index], context.temp_allocator) + symbol.comment = get_comment(value.comments[index], context.temp_allocator) symbol.signature = get_bit_field_field_signature(value, index) symbol.range = value.ranges[index] } construct_enum_field_symbol :: proc(symbol: ^Symbol, value: SymbolEnumValue, index: int) { symbol.type = .Field - symbol.doc = get_doc(value.docs[index], context.temp_allocator) - symbol.comment = get_comment(value.comments[index]) + symbol.doc = get_comment(value.docs[index], context.temp_allocator) + symbol.comment = get_comment(value.comments[index], context.temp_allocator) symbol.signature = get_enum_field_signature(value, index) symbol.range = value.ranges[index] } @@ -944,7 +956,7 @@ construct_ident_symbol_info :: proc(symbol: ^Symbol, ident: string, document_pkg symbol.type_name = symbol.name symbol.type_pkg = symbol.pkg symbol.name = ident - if symbol.type == .Variable { + if symbol.type == .Variable || symbol.type == .Constant { symbol.pkg = document_pkg } diff --git a/src/server/types.odin b/src/server/types.odin index 4b4c0bd..c65e181 100644 --- a/src/server/types.odin +++ b/src/server/types.odin @@ -277,10 +277,10 @@ DiagnosticSeverity :: enum { Hint = 4, } - DiagnosticTag :: enum int { +DiagnosticTag :: enum int { Unnecessary = 1, Deprecated = 2, - } +} Diagnostic :: struct { range: common.Range, @@ -413,35 +413,38 @@ FileSystemWatcher :: struct { } OlsConfig :: struct { - collections: [dynamic]OlsConfigCollection, - thread_pool_count: Maybe(int), - enable_format: Maybe(bool), - enable_hover: Maybe(bool), - enable_document_symbols: Maybe(bool), - enable_fake_methods: Maybe(bool), - enable_references: Maybe(bool), - enable_document_highlights: Maybe(bool), - enable_document_links: Maybe(bool), - enable_completion_matching: Maybe(bool), - enable_inlay_hints_params: Maybe(bool), - enable_inlay_hints_default_params: Maybe(bool), - enable_inlay_hints_implicit_return: Maybe(bool), - enable_semantic_tokens: Maybe(bool), - enable_unused_imports_reporting: Maybe(bool), - enable_procedure_context: Maybe(bool), - enable_snippets: Maybe(bool), - enable_procedure_snippet: Maybe(bool), - enable_checker_only_saved: Maybe(bool), - enable_auto_import: Maybe(bool), - disable_parser_errors: Maybe(bool), - verbose: Maybe(bool), - file_log: Maybe(bool), - odin_command: string, - odin_root_override: string, - checker_args: string, - checker_targets: []string, - profiles: [dynamic]common.ConfigProfile, - profile: string, + collections: [dynamic]OlsConfigCollection, + thread_pool_count: Maybe(int), + enable_format: Maybe(bool), + enable_hover: Maybe(bool), + enable_document_symbols: Maybe(bool), + enable_fake_methods: Maybe(bool), + enable_overload_resolution: Maybe(bool), + enable_references: Maybe(bool), + enable_document_highlights: Maybe(bool), + enable_document_links: Maybe(bool), + enable_comp_lit_signature_help: Maybe(bool), + enable_comp_lit_signature_help_use_docs: Maybe(bool), + enable_completion_matching: Maybe(bool), + enable_inlay_hints_params: Maybe(bool), + enable_inlay_hints_default_params: Maybe(bool), + enable_inlay_hints_implicit_return: Maybe(bool), + enable_semantic_tokens: Maybe(bool), + enable_unused_imports_reporting: Maybe(bool), + enable_procedure_context: Maybe(bool), + enable_snippets: Maybe(bool), + enable_procedure_snippet: Maybe(bool), + enable_checker_only_saved: Maybe(bool), + enable_auto_import: Maybe(bool), + disable_parser_errors: Maybe(bool), + verbose: Maybe(bool), + file_log: Maybe(bool), + odin_command: string, + odin_root_override: string, + checker_args: string, + checker_targets: []string, + profiles: [dynamic]common.ConfigProfile, + profile: string, } OlsConfigCollection :: struct { diff --git a/src/server/unmarshal.odin b/src/server/unmarshal.odin index d93663c..9025428 100644 --- a/src/server/unmarshal.odin +++ b/src/server/unmarshal.odin @@ -1,9 +1,8 @@ +#+feature using-stmt package server import "base:runtime" - import "core:encoding/json" -import "core:fmt" import "core:mem" import "core:strings" diff --git a/src/server/when.odin b/src/server/when.odin index 20ef128..ea1d397 100644 --- a/src/server/when.odin +++ b/src/server/when.odin @@ -3,7 +3,6 @@ package server import "base:runtime" import "core:fmt" -import "core:log" import "core:odin/ast" import "core:strconv" diff --git a/src/server/workspace_symbols.odin b/src/server/workspace_symbols.odin index 11e7a8a..de346d1 100644 --- a/src/server/workspace_symbols.odin +++ b/src/server/workspace_symbols.odin @@ -1,7 +1,6 @@ package server import "core:fmt" -import "core:log" import "core:os" import "core:path/filepath" import "core:strings" @@ -12,32 +11,13 @@ import "src:common" dir_blacklist :: []string{"node_modules", ".git"} WorkspaceCache :: struct { - time: time.Time, - pkgs: [dynamic]string, + time: time.Time, + pkgs: [dynamic]string, } @(thread_local, private = "file") cache: WorkspaceCache -@(private) -walk_dir :: proc(info: os.File_Info, in_err: os.Errno, user_data: rawptr) -> (err: os.Error, skip_dir: bool) { - pkgs := cast(^[dynamic]string)user_data - - if info.is_dir { - dir, _ := filepath.to_slash(info.fullpath, context.temp_allocator) - dir_name := filepath.base(dir) - - for blacklist in dir_blacklist { - if blacklist == dir_name { - return nil, true - } - } - append(pkgs, dir) - } - - return nil, false -} - get_workspace_symbols :: proc(query: string) -> (workspace_symbols: []WorkspaceSymbol, ok: bool) { if time.since(cache.time) > 20 * time.Second { for pkg in cache.pkgs { @@ -48,7 +28,25 @@ get_workspace_symbols :: proc(query: string) -> (workspace_symbols: []WorkspaceS uri := common.parse_uri(workspace.uri, context.temp_allocator) or_return pkgs := make([dynamic]string, 0, context.temp_allocator) - filepath.walk(uri.path, walk_dir, &pkgs) + w := os.walker_create(uri.path) + defer os.walker_destroy(&w) + for info in os.walker_walk(&w) { + if info.type == .Directory { + dir := strings.clone(info.fullpath, context.temp_allocator) + dir_name := filepath.base(dir) + found := false + for blacklist in dir_blacklist { + if blacklist == dir_name { + found = true + os.walker_skip_dir(&w) + break + } + } + if !found { + append(&pkgs, dir) + } + } + } _pkg: for pkg in pkgs { matches, err := filepath.glob(fmt.tprintf("%v/*.odin", pkg), context.temp_allocator) @@ -58,7 +56,7 @@ get_workspace_symbols :: proc(query: string) -> (workspace_symbols: []WorkspaceS } for exclude_path in common.config.profile.exclude_path { - exclude_forward, _ := filepath.to_slash(exclude_path, context.temp_allocator) + exclude_forward, _ := filepath.replace_path_separators(exclude_path, '/', context.temp_allocator) if exclude_forward[len(exclude_forward) - 2:] == "**" { lower_pkg := strings.to_lower(pkg) diff --git a/src/testing/testing.odin b/src/testing/testing.odin index 4cb9484..5e927a7 100644 --- a/src/testing/testing.odin +++ b/src/testing/testing.odin @@ -2,7 +2,6 @@ package ols_testing import "core:fmt" import "core:log" -import "core:mem" import "core:mem/virtual" import "core:odin/ast" import "core:odin/parser" @@ -69,6 +68,9 @@ setup :: proc(src: ^Source) { server.setup_index() + // Set the collection's config to the test's config to enable feature flags like enable_fake_method + server.indexer.index.collection.config = &src.config + server.document_setup(src.document) server.document_refresh(src.document, &src.config, nil) @@ -127,7 +129,7 @@ expect_signature_labels :: proc(t: ^testing.T, src: ^Source, expect_labels: []st setup(src) defer teardown(src) - help, ok := server.get_signature_information(src.document, src.position) + help, ok := server.get_signature_information(src.document, src.position, &src.config) if !ok { log.error("Failed get_signature_information") @@ -159,14 +161,20 @@ expect_signature_parameter_position :: proc(t: ^testing.T, src: ^Source, positio setup(src) defer teardown(src) - help, ok := server.get_signature_information(src.document, src.position) + help, ok := server.get_signature_information(src.document, src.position, &src.config) if help.activeParameter != position { log.errorf("expected parameter position %v, but received %v", position, help.activeParameter) } } -expect_completion_labels :: proc(t: ^testing.T, src: ^Source, trigger_character: string, expect_labels: []string) { +expect_completion_labels :: proc( + t: ^testing.T, + src: ^Source, + trigger_character: string, + expect_labels: []string, + expect_excluded: []string = nil, +) { setup(src) defer teardown(src) @@ -199,6 +207,14 @@ expect_completion_labels :: proc(t: ^testing.T, src: ^Source, trigger_character: log.errorf("Expected completion detail %v, but received %v", expect_labels[i], completion_list.items) } } + + for expect_exclude in expect_excluded { + for completion in completion_list.items { + if expect_exclude == completion.label { + log.errorf("Expected completion label %v to not be included", expect_exclude) + } + } + } } expect_completion_docs :: proc( @@ -305,6 +321,50 @@ expect_completion_insert_text :: proc( } } +expect_completion_edit_text :: proc( + t: ^testing.T, + src: ^Source, + trigger_character: string, + label: string, + expected_text: string, +) { + setup(src) + defer teardown(src) + + completion_context := server.CompletionContext { + triggerCharacter = trigger_character, + } + + completion_list, ok := server.get_completion_list(src.document, src.position, completion_context, &src.config) + + if !ok { + log.error("Failed get_completion_list") + } + + found := false + for completion in completion_list.items { + if completion.label == label { + found = true + if text_edit, has_edit := completion.textEdit.(server.TextEdit); has_edit { + if text_edit.newText != expected_text { + log.errorf( + "Completion '%v' expected textEdit.newText %q, but received %q", + label, + expected_text, + text_edit.newText, + ) + } + } else { + log.errorf("Completion '%v' has no textEdit", label) + } + break + } + } + if !found { + log.errorf("Expected completion label '%v' not found in %v", label, completion_list.items) + } +} + expect_hover :: proc(t: ^testing.T, src: ^Source, expect_hover_string: string) { setup(src) defer teardown(src) @@ -333,7 +393,7 @@ expect_definition_locations :: proc(t: ^testing.T, src: ^Source, expect_location setup(src) defer teardown(src) - locations, ok := server.get_definition_location(src.document, src.position) + locations, ok := server.get_definition_location(src.document, src.position, &src.config) if !ok { log.error("Failed get_definition_location") @@ -463,7 +523,10 @@ expect_action :: proc(t: ^testing.T, src: ^Source, expect_action_names: []string setup(src) defer teardown(src) - input_range := common.Range{start=src.position, end=src.position} + input_range := common.Range { + start = src.position, + end = src.position, + } actions, ok := server.get_code_actions(src.document, input_range, &src.config) if !ok { log.error("Failed to find actions") @@ -490,6 +553,44 @@ expect_action :: proc(t: ^testing.T, src: ^Source, expect_action_names: []string } } +expect_action_with_edit :: proc(t: ^testing.T, src: ^Source, action_name: string, expected_new_text: string) { + setup(src) + defer teardown(src) + + input_range := common.Range { + start = src.position, + end = src.position, + } + actions, ok := server.get_code_actions(src.document, input_range, &src.config) + if !ok { + log.error("Failed to find actions") + return + } + + for action in actions { + if action.title == action_name { + // Get the text edit for the document + if edits, found := action.edit.changes[src.document.uri.uri]; found { + if len(edits) > 0 { + actual_text := edits[0].newText + testing.expectf( + t, + actual_text == expected_new_text, + "\nExpected edit text:\n%s\n\nGot:\n%s", + expected_new_text, + actual_text, + ) + return + } + } + log.errorf("Action '%s' found but has no edits", action_name) + return + } + } + + log.errorf("Action '%s' not found in actions: %v", action_name, actions) +} + expect_semantic_tokens :: proc(t: ^testing.T, src: ^Source, expected: []server.SemanticToken) { setup(src) defer teardown(src) @@ -537,31 +638,30 @@ expect_inlay_hints :: proc(t: ^testing.T, src: ^Source) { src_builder := strings.builder_make(context.temp_allocator) expected_hints := make([dynamic]server.InlayHint, context.temp_allocator) - HINT_OPEN :: "[[" + HINT_OPEN :: "[[" HINT_CLOSE :: "]]" { last, line, col: int saw_brackets: bool - for i:= 0; i < len(src.main); i += 1 { + for i := 0; i < len(src.main); i += 1 { if saw_brackets { - if i+1 < len(src.main) && src.main[i:][:len(HINT_CLOSE)] == HINT_CLOSE { + if i + 1 < len(src.main) && src.main[i:][:len(HINT_CLOSE)] == HINT_CLOSE { saw_brackets = false hint_str := src.main[last:i] - last = i+len(HINT_CLOSE) - i = last-1 - append(&expected_hints, server.InlayHint{ - position = {line, col}, - label = hint_str, - kind = .Parameter, - }) + last = i + len(HINT_CLOSE) + i = last - 1 + append( + &expected_hints, + server.InlayHint{position = {line, col}, label = hint_str, kind = .Parameter}, + ) } } else { - if i+1 < len(src.main) && src.main[i:][:len(HINT_OPEN)] == HINT_OPEN { + if i + 1 < len(src.main) && src.main[i:][:len(HINT_OPEN)] == HINT_OPEN { strings.write_string(&src_builder, src.main[last:i]) saw_brackets = true - last = i+len(HINT_OPEN) - i = last-1 + last = i + len(HINT_OPEN) + i = last - 1 } else if src.main[i] == '\n' { line += 1 col = 0 @@ -584,7 +684,7 @@ expect_inlay_hints :: proc(t: ^testing.T, src: ^Source) { setup(src) defer teardown(src) - symbols_and_nodes := server.resolve_entire_file(src.document, allocator=context.temp_allocator) + symbols_and_nodes := server.resolve_entire_file(src.document, allocator = context.temp_allocator) range := common.Range { end = {line = 9000000}, @@ -595,7 +695,8 @@ expect_inlay_hints :: proc(t: ^testing.T, src: ^Source) { return } - testing.expectf(t, + testing.expectf( + t, len(expected_hints) == len(hints), "Expected %d inlay hints, but received %d", len(expected_hints), @@ -620,20 +721,30 @@ expect_inlay_hints :: proc(t: ^testing.T, src: ^Source) { for i in 0 ..< max(len(expected_hints), len(hints)) { expected_text := "---" - actual_text := "---" + actual_text := "---" if i < len(expected_hints) { expected := expected_hints[i] expected_line := get_source_line_with_hint(lines, expected) - expected_text = fmt.tprintf("\"%s\" at (%d, %d): \"%s\"", - expected.label, expected.position.line, expected.position.character, expected_line) + expected_text = fmt.tprintf( + "\"%s\" at (%d, %d): \"%s\"", + expected.label, + expected.position.line, + expected.position.character, + expected_line, + ) } if i < len(hints) { actual := hints[i] actual_line := get_source_line_with_hint(lines, actual) - actual_text = fmt.tprintf("\"%s\" at (%d, %d): \"%s\"", - actual.label, actual.position.line, actual.position.character, actual_line) + actual_text = fmt.tprintf( + "\"%s\" at (%d, %d): \"%s\"", + actual.label, + actual.position.line, + actual.position.character, + actual_line, + ) } if i >= len(expected_hints) { diff --git a/tests/action_invert_if_test.odin b/tests/action_invert_if_test.odin new file mode 100644 index 0000000..bb12ad9 --- /dev/null +++ b/tests/action_invert_if_test.odin @@ -0,0 +1,400 @@ +package tests + +import "core:testing" + +import test "src:testing" + +INVERT_IF_ACTION :: "Invert if" + +@(test) +action_invert_if_simple :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + +main :: proc() { + x := 5 + if x{*} >= 0 { + foo() + } +} +`, + packages = {}, + } + + test.expect_action(t, &source, {INVERT_IF_ACTION}) +} + +@(test) +action_invert_if_simple_edit :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + +main :: proc() { + x := 5 + if x{*} >= 0 { + foo() + } +} +`, + packages = {}, + } + + expected := `if x < 0 { + } else { + foo() + }` + + test.expect_action_with_edit(t, &source, INVERT_IF_ACTION, expected) +} + +@(test) +action_invert_if_with_else :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + +main :: proc() { + x := 5 + if x{*} == 0 { + foo() + } else { + bar() + } +} +`, + packages = {}, + } + + test.expect_action(t, &source, {INVERT_IF_ACTION}) +} + +@(test) +action_invert_if_with_else_edit :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + +main :: proc() { + x := 5 + if x{*} == 0 { + foo() + } else { + bar() + } +} +`, + packages = {}, + } + + expected := `if x != 0 { + bar() + } else { + foo() + }` + + test.expect_action_with_edit(t, &source, INVERT_IF_ACTION, expected) +} + +@(test) +action_invert_if_with_init :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + +main :: proc() { + if x{*} := foo(); x < 0 { + bar() + } +} +`, + packages = {}, + } + + test.expect_action(t, &source, {INVERT_IF_ACTION}) +} + +@(test) +action_invert_if_with_init_edit :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + +main :: proc() { + if x{*} := foo(); x < 0 { + bar() + } +} +`, + packages = {}, + } + + expected := `if x := foo(); x >= 0 { + } else { + bar() + }` + + test.expect_action_with_edit(t, &source, INVERT_IF_ACTION, expected) +} + +@(test) +action_invert_if_not_on_if :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + +main :: proc() { + x :={*} 5 +} +`, + packages = {}, + } + + // Should not have the invert action when not on an if statement + test.expect_action(t, &source, {}) +} + + +@(test) +action_invert_if_inside_of_statement :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + +main :: proc() { + if x != 0 { + foo{*}() + } +} +`, + packages = {}, + } + + test.expect_action(t, &source, {}) +} + +@(test) +action_invert_if_not_eq :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + +main :: proc() { + if x{*} != 0 { + foo() + } +} +`, + packages = {}, + } + + expected := `if x == 0 { + } else { + foo() + }` + + test.expect_action_with_edit(t, &source, INVERT_IF_ACTION, expected) +} + +@(test) +action_invert_if_lt :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + +main :: proc() { + if x{*} < 5 { + foo() + } +} +`, + packages = {}, + } + + expected := `if x >= 5 { + } else { + foo() + }` + + test.expect_action_with_edit(t, &source, INVERT_IF_ACTION, expected) +} + +@(test) +action_invert_if_gt :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + +main :: proc() { + if x{*} > 5 { + foo() + } +} +`, + packages = {}, + } + + expected := `if x <= 5 { + } else { + foo() + }` + + test.expect_action_with_edit(t, &source, INVERT_IF_ACTION, expected) +} + +@(test) +action_invert_if_le :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + +main :: proc() { + if x{*} <= 5 { + foo() + } +} +`, + packages = {}, + } + + expected := `if x > 5 { + } else { + foo() + }` + + test.expect_action_with_edit(t, &source, INVERT_IF_ACTION, expected) +} + +@(test) +action_invert_if_negated :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + +main :: proc() { + if !x{*} { + foo() + } +} +`, + packages = {}, + } + + expected := `if x { + } else { + foo() + }` + + test.expect_action_with_edit(t, &source, INVERT_IF_ACTION, expected) +} + +@(test) +action_invert_if_boolean :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + +main :: proc() { + if x{*} { + foo() + } +} +`, + packages = {}, + } + + expected := `if !x { + } else { + foo() + }` + + test.expect_action_with_edit(t, &source, INVERT_IF_ACTION, expected) +} + +@(test) +action_invert_if_else_if_chain :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + +main :: proc() { + x := something() + if x{*} > 0 { + statement1() + } else if x < 0 { + statement2() + } else { + statement3() + } +} +`, + packages = {}, + } + + expected := `if x <= 0 { + if x < 0 { + statement2() + } else { + statement3() + } + } else { + statement1() + }` + + test.expect_action_with_edit(t, &source, INVERT_IF_ACTION, expected) +} + +@(test) +action_invert_if_not_on_else_if :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + +main :: proc() { + x := something() + if x > 0 { + statement1() + } else if x{*} < 0 { + statement2() + } else { + statement3() + } +} +`, + packages = {}, + } + + // Should not have the invert action when on an else-if statement + test.expect_action(t, &source, {}) +} + +@(test) +action_invert_if_not_on_else :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + +main :: proc() { + x := something() + if x > 0 { + statement1() + } else { + statement3(){*} + } +} +`, + packages = {}, + } + + // Should not have the invert action when in the else block (not on an if) + test.expect_action(t, &source, {}) +} + +@(test) +action_invert_if_nested_in_else_if_body :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + +main :: proc() { + x := something() + if x > 0 { + statement1() + } else if x < 0 { + if y{*} > 0 { + statement2() + } + } else { + statement3() + } +} +`, + packages = {}, + } + + // Should have the invert action for an if statement nested inside an else-if body + test.expect_action(t, &source, {INVERT_IF_ACTION}) +} diff --git a/tests/actions_test.odin b/tests/actions_test.odin new file mode 100644 index 0000000..935e168 --- /dev/null +++ b/tests/actions_test.odin @@ -0,0 +1,23 @@ +package tests + +import "core:testing" + +import test "src:testing" + +@(test) +action_remove_unsed_import_when_stmt :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + import "core:fm{*}t" + + when true { + main :: proc() { + _ = fmt.printf + } + } + `, + packages = {}, + } + + test.expect_action(t, &source, {}) +} diff --git a/tests/completions_test.odin b/tests/completions_test.odin index 72dcb86..bf05f6a 100644 --- a/tests/completions_test.odin +++ b/tests/completions_test.odin @@ -28,7 +28,7 @@ ast_simple_struct_completion :: proc(t: ^testing.T) { t, &source, ".", - {"My_Struct.one: int", "My_Struct.two: int\n// test comment", "My_Struct.three: int"}, + {"My_Struct.one: int", "My_Struct.two: int\n---\ntest comment", "My_Struct.three: int"}, ) } @@ -3296,7 +3296,7 @@ ast_completion_struct_documentation :: proc(t: ^testing.T) { packages = packages[:], } - test.expect_completion_docs(t, &source, "", {"Foo.bazz: my_package.My_Struct\n// bazz"}) + test.expect_completion_docs(t, &source, "", {"Foo.bazz: my_package.My_Struct\n---\nbazz"}) } @(test) @@ -3417,7 +3417,7 @@ ast_completion_poly_struct_another_package :: proc(t: ^testing.T) { packages = packages[:], } - test.expect_completion_docs(t, &source, "", {"Runner.state: test.State\n// state"}) + test.expect_completion_docs(t, &source, "", {"Runner.state: test.State\n---\nstate"}) } @(test) @@ -5130,3 +5130,401 @@ ast_completion_implicit_selector_binary_expr :: proc(t: ^testing.T) { } test.expect_completion_docs(t, &source, "", {"A", "B"}) } + +@(test) +ast_completion_global_selector_from_local_scope :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + Foo :: struct { + foo: int, + } + + FOO :: Foo{} + + main :: proc() { + FOO.{*} + } + `, + } + test.expect_completion_docs(t, &source, "", {"Foo.foo: int"}) +} + +@(test) +ast_completion_empty_selector_with_ident_newline :: proc(t: ^testing.T) { + packages := make([dynamic]test.Package, context.temp_allocator) + + append( + &packages, + test.Package { + pkg = "my_package", + source = `package my_package + Foo :: struct{} + `, + }, + ) + source := test.Source { + main = `package test + import "my_package" + + main :: proc() { + my_package.{*} + y := 2 + } + `, + packages = packages[:], + } + test.expect_completion_docs(t, &source, "", {"my_package.Foo :: struct{}"}) +} + +@(test) +ast_completion_implicit_selector_binary_expr_proc_call :: proc(t: ^testing.T) { + packages := make([dynamic]test.Package, context.temp_allocator) + + append( + &packages, + test.Package { + pkg = "my_package", + source = `package my_package + Foo :: enum { + A, + B, + C, + } + + Bar :: enum { + X, + Y, + } + + foo :: proc(f: Foo) -> bit_set[Bar] { + return {.X} + } + `, + }, + ) + source := test.Source { + main = `package test + import "my_package" + + main :: proc() { + results: bit_set[my_package.Bar] + + results |= my_package.foo(.{*}) + } + `, + packages = packages[:], + } + test.expect_completion_labels(t, &source, "", {"A", "B", "C"}, {"X", "Y"}) +} + +@(test) +ast_completion_proc_arg_default_enum_alias :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + Foo :: enum { + A, + B, + } + + Bar :: Foo.A + + foo :: proc(f := Bar) {} + + main :: proc() { + foo(.{*}) + } + `, + } + test.expect_completion_docs(t, &source, "", {"A", "B"}) +} + +@(test) +ast_completion_proc_group_bitset :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + Foo :: enum { + A, + B, + } + + Foos :: bit_set[Foo] + + foo_one :: proc(i: int, foos: Foos) {} + foo_two :: proc(s: string, foos: Foos) {} + foo :: proc { + foo_one, + foo_two, + } + + main :: proc() { + foo(1, {.{*}}) + } + `, + } + test.expect_completion_docs(t, &source, "", {"A", "B"}) +} + +@(test) +ast_completion_struct_using_anonymous_vector_types :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + Foo :: struct { + using _: [3]f32, + } + + main :: proc() { + foo: Foo + foo.{*} + } + + `, + } + test.expect_completion_docs(t, &source, "", {"r: f32", "x: f32"}) +} + +@(test) +ast_completion_struct_using_named_vector_types :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + Foo :: struct { + using bar: [3]f32, + } + + main :: proc() { + foo: Foo + foo.{*} + } + + `, + } + test.expect_completion_docs(t, &source, "", {"Foo.bar: [3]f32", "r: f32", "x: f32"}) +} + +@(test) +ast_completion_parapoly_struct_with_parapoly_child :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + SomeEnum :: enum { + enumVal1, + enumVal2 + } + + ChildStruct:: struct($enumGeneric: typeid){ + Something : string, + GenericParam: enumGeneric + } + + ParentStruct :: struct($enumGeneric: typeid){ + ParentSomething: string, + Child: ChildStruct(enumGeneric) + } + + TestGenericStructs :: proc(){ + parent : ParentStruct(SomeEnum) = {}; + parent.Child.{*} + } + `, + } + test.expect_completion_docs(t, &source, "", {"ChildStruct.GenericParam: test.SomeEnum", "ChildStruct.Something: string"}) +} + +@(test) +ast_completion_fake_method_simple :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + import "methods" + main :: proc() { + n: int + n.{*} + } + `, + packages = { + { + pkg = "methods", + source = `package methods + double :: proc(x: int) -> int { return x * 2 } + `, + }, + }, + config = {enable_fake_method = true}, + } + // Should show 'double' as a fake method for int + test.expect_completion_labels(t, &source, ".", {"double"}) +} + +@(test) +ast_completion_fake_method_proc_group :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + import "methods" + main :: proc() { + n: int + n.{*} + } + `, + packages = { + { + pkg = "methods", + source = `package methods + add_int :: proc(a, b: int) -> int { return a + b } + add_something :: proc(a: int, b: string) {} + add_float :: proc(a, b: f32) -> f32 { return a + b } + add :: proc { add_float, add_int, add_something } + `, + }, + }, + config = {enable_fake_method = true}, + } + // Should show 'add' (the proc group), not 'add_int' or 'add_something' (individual procs) + test.expect_completion_labels(t, &source, ".", {"add"}, {"add_int", "add_something"}) +} + +@(test) +ast_completion_fake_method_proc_group_only_shows_group :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + import "methods" + main :: proc() { + s: methods.My_Struct + s.{*} + } + `, + packages = { + { + pkg = "methods", + source = `package methods + My_Struct :: struct { x: int } + + do_thing_int :: proc(s: My_Struct, v: int) {} + do_thing_str :: proc(s: My_Struct, v: string) {} + do_thing :: proc { do_thing_int, do_thing_str } + + // standalone proc not in a group + standalone_method :: proc(s: My_Struct) {} + `, + }, + }, + config = {enable_fake_method = true}, + } + // Should show 'do_thing' (group) and 'standalone_method', but NOT 'do_thing_int' or 'do_thing_str' + test.expect_completion_labels(t, &source, ".", {"do_thing", "standalone_method"}, {"do_thing_int", "do_thing_str"}) +} + +@(test) +ast_completion_fake_method_proc_group_with_only_one_proc :: proc(t: ^testing.T) { + // This is to verify that even if a proc group has only one member, + // it still shows up as a group and does not show the individual proc. + source := test.Source { + main = `package test + import "methods" + main :: proc() { + s: methods.My_Struct + s.{*} + } + `, + packages = { + { + pkg = "methods", + source = `package methods + My_Struct :: struct { x: int } + + do_thing_int :: proc(s: My_Struct, v: int) {} + do_thing :: proc { do_thing_int } + + // standalone proc not in a group + standalone_method :: proc(s: My_Struct) {} + `, + }, + }, + config = {enable_fake_method = true}, + } + + test.expect_completion_labels(t, &source, ".", {"do_thing", "standalone_method"}, {"do_thing_int" }) +} + +@(test) +ast_completion_fake_method_builtin_type_uses_builtin_pkg :: proc(t: ^testing.T) { + // This test verifies that fake methods for builtin types (int, f32, string, etc.) + // are correctly looked up using "$builtin" as the package, not the package where + // the variable is declared. Without this fix, the method lookup would fail because: + // - Storage: method stored with key {pkg = "$builtin", name = "int"} + // - Lookup (wrong): would use {pkg = "test", name = "int"} based on variable's declaring package + // - Lookup (correct): uses {pkg = "$builtin", name = "int"} for builtin types + source := test.Source { + main = `package test + import "math_utils" + main :: proc() { + x: f32 + x.{*} + } + `, + packages = { + { + pkg = "math_utils", + source = `package math_utils + square :: proc(v: f32) -> f32 { return v * v } + cube :: proc(v: f32) -> f32 { return v * v * v } + `, + }, + }, + config = {enable_fake_method = true}, + } + // Both methods should appear as fake methods for f32, proving that + // the lookup correctly uses "$builtin" instead of "test" for the package + test.expect_completion_labels(t, &source, ".", {"square", "cube"}) +} + +@(test) +ast_completion_fake_method_proc_group_single_arg_cursor_position :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + import "methods" + main :: proc() { + n: int + n.{*} + } + `, + packages = { + { + pkg = "methods", + source = `package methods + // All members only take a single argument (the receiver) + negate_a :: proc(x: int) -> int { return -x } + negate_b :: proc(x: int) -> int { return 0 - x } + negate :: proc { negate_a, negate_b } + `, + }, + }, + config = {enable_fake_method = true}, + } + // The proc group 'negate' should have cursor AFTER parentheses since no additional args + test.expect_completion_edit_text(t, &source, ".", "negate", "methods.negate(n)$0") +} + +@(test) +ast_completion_package_docs :: proc(t: ^testing.T) { + packages := make([dynamic]test.Package, context.temp_allocator) + + append( + &packages, + test.Package { + pkg = "my_package", + source = `// Package docs + package my_package + Foo :: struct{} + `, + }, + ) + + source := test.Source { + main = `package test + import "my_package" + main :: proc() { + my_pack{*} + } + `, + packages = packages[:], + } + + test.expect_completion_docs(t, &source, "", {"my_package: package\n---\nPackage docs"}) +} diff --git a/tests/definition_test.odin b/tests/definition_test.odin index 4810361..fe4cd3b 100644 --- a/tests/definition_test.odin +++ b/tests/definition_test.odin @@ -723,3 +723,56 @@ ast_goto_package_declaration_with_alias :: proc(t: ^testing.T) { test.expect_definition_locations(t, &source, locations[:]) } +@(test) +ast_goto_proc_group_overload_with_selector :: proc(t: ^testing.T) { + packages := make([dynamic]test.Package, context.temp_allocator) + + append(&packages, test.Package{pkg = "my_package", source = `package my_package + push_back :: proc(arr: ^[dynamic]int, val: int) {} + push_back_elems :: proc(arr: ^[dynamic]int, vals: ..int) {} + append :: proc{push_back, push_back_elems} + `}) + source := test.Source { + main = `package test + import mp "my_package" + + main :: proc() { + arr: [dynamic]int + mp.app{*}end(&arr, 1) + } + `, + packages = packages[:], + config = {enable_overload_resolution = true}, + } + // Should go to push_back (line 1, character 3) instead of append (line 3) + // because push_back is the overload being used with a single value argument + locations := []common.Location { + {range = {start = {line = 1, character = 3}, end = {line = 1, character = 12}}}, + } + + test.expect_definition_locations(t, &source, locations[:]) +} + +@(test) +ast_goto_proc_group_overload_identifier :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + push_back :: proc(arr: ^[dynamic]int, val: int) {} + push_back_elems :: proc(arr: ^[dynamic]int, vals: ..int) {} + append :: proc{push_back, push_back_elems} + + main :: proc() { + arr: [dynamic]int + app{*}end(&arr, 1) + } + `, + config = {enable_overload_resolution = true}, + } + // Should go to push_back (line 1, character 2) instead of append (line 3) + // because push_back is the overload being used with a single value argument + locations := []common.Location { + {range = {start = {line = 1, character = 2}, end = {line = 1, character = 11}}}, + } + + test.expect_definition_locations(t, &source, locations[:]) +}
\ No newline at end of file diff --git a/tests/hover_test.odin b/tests/hover_test.odin index 8a54ad4..c207da1 100644 --- a/tests/hover_test.odin +++ b/tests/hover_test.odin @@ -401,7 +401,7 @@ ast_hover_proc_group :: proc(t: ^testing.T) { packages = {}, } - test.expect_hover(t, &source, "test.add :: proc(a, b: int) -> int\n docs\n\n// comment") + test.expect_hover(t, &source, "test.add :: proc(a, b: int) -> int\n---\ndocs\n---\ncomment") } @(test) @@ -672,7 +672,7 @@ ast_hover_struct_field_complex_definition :: proc(t: ^testing.T) { `, } - test.expect_hover(t, &source, "Foo.bar: ^test.Bar\n Docs\n\n// inline docs") + test.expect_hover(t, &source, "Foo.bar: ^test.Bar\n---\nDocs\n---\ninline docs") } @(test) @@ -1084,7 +1084,7 @@ ast_hover_proc_overloading_named_arg_with_selector_expr_with_another_package :: packages = packages[:], } - test.expect_hover(t, &source, "my_package.foo :: proc(x := 1) -> (_: int, _: bool)\n Docs\n\n// comment") + test.expect_hover(t, &source, "my_package.foo :: proc(x := 1) -> (_: int, _: bool)\n---\nDocs\n---\ncomment") } @(test) @@ -1479,7 +1479,7 @@ ast_hover_proc_comments :: proc(t: ^testing.T) { `, } - test.expect_hover(t, &source, "test.foo :: proc()\n doc\n\n// do foo") + test.expect_hover(t, &source, "test.foo :: proc()\n---\ndoc\n---\ndo foo") } @(test) @@ -1507,7 +1507,7 @@ ast_hover_proc_comments_package :: proc(t: ^testing.T) { packages = packages[:], } - test.expect_hover(t, &source, "my_package.foo :: proc()\n// do foo") + test.expect_hover(t, &source, "my_package.foo :: proc()\n---\ndo foo") } @(test) @@ -1525,7 +1525,7 @@ ast_hover_struct_field_distinct :: proc(t: ^testing.T) { `, } - test.expect_hover(t, &source, "S.fb: test.B\n// type: fb") + test.expect_hover(t, &source, "S.fb: test.B\n---\ntype: fb") } @(test) @@ -2135,7 +2135,7 @@ ast_hover_bit_field_field :: proc(t: ^testing.T) { } `, } - test.expect_hover(t, &source, "Foo.foo_aa: uint | 6\n// last 6 bits") + test.expect_hover(t, &source, "Foo.foo_aa: uint | 6\n---\nlast 6 bits") } @(test) @@ -2154,7 +2154,7 @@ ast_hover_bit_field_variable_with_docs :: proc(t: ^testing.T) { } `, } - test.expect_hover(t, &source, "Foo.foo_a: uint | 2\n doc\n\n// foo a") + test.expect_hover(t, &source, "Foo.foo_a: uint | 2\n---\ndoc\n---\nfoo a") } @(test) @@ -2353,7 +2353,7 @@ ast_hover_struct_field_should_show_docs_and_comments :: proc(t: ^testing.T) { } `, } - test.expect_hover(t, &source, "Foo.a: int\n a docs\n\n// a comment") + test.expect_hover(t, &source, "Foo.a: int\n---\na docs\n---\na comment") } @(test) @@ -2367,7 +2367,7 @@ ast_hover_struct_field_should_show_docs_and_comments_field :: proc(t: ^testing.T } `, } - test.expect_hover(t, &source, "Foo.a: int\n a docs\n\n// a comment") + test.expect_hover(t, &source, "Foo.a: int\n---\na docs\n---\na comment") } @(test) @@ -2388,7 +2388,7 @@ ast_hover_struct_field_should_show_docs_and_comments_struct_types :: proc(t: ^te } `, } - test.expect_hover(t, &source, "Foo.bar: test.Bar\n bar docs\n\n// bar comment") + test.expect_hover(t, &source, "Foo.bar: test.Bar\n---\nbar docs\n---\nbar comment") } @(test) @@ -2407,7 +2407,7 @@ ast_hover_struct_field_should_show_docs_and_comments_procs :: proc(t: ^testing.T } `, } - test.expect_hover(t, &source, "Foo.bar: proc(a: int) -> int\n bar docs\n\n// bar comment") + test.expect_hover(t, &source, "Foo.bar: proc(a: int) -> int\n---\nbar docs\n---\nbar comment") } @(test) @@ -2428,7 +2428,7 @@ ast_hover_struct_field_should_show_docs_and_comments_named_procs :: proc(t: ^tes } `, } - test.expect_hover(t, &source, "Foo.bar: proc(a: int) -> string\n bar docs\n\n// bar comment") + test.expect_hover(t, &source, "Foo.bar: proc(a: int) -> string\n---\nbar docs\n---\nbar comment") } @(test) @@ -2447,7 +2447,7 @@ ast_hover_struct_field_should_show_docs_and_comments_maps :: proc(t: ^testing.T) } `, } - test.expect_hover(t, &source, "Foo.bar: map[int]int\n bar docs\n\n// bar comment") + test.expect_hover(t, &source, "Foo.bar: map[int]int\n---\nbar docs\n---\nbar comment") } @(test) @@ -2466,7 +2466,7 @@ ast_hover_struct_field_should_show_docs_and_comments_bit_sets :: proc(t: ^testin } `, } - test.expect_hover(t, &source, "Foo.bar: bit_set[0 ..< 10]\n bar docs\n\n// bar comment") + test.expect_hover(t, &source, "Foo.bar: bit_set[0 ..< 10]\n---\nbar docs\n---\nbar comment") } @(test) @@ -2490,7 +2490,7 @@ ast_hover_struct_field_should_show_docs_and_comments_unions :: proc(t: ^testing. } `, } - test.expect_hover(t, &source, "Foo.bar: test.Bar\n bar docs\n\n// bar comment") + test.expect_hover(t, &source, "Foo.bar: test.Bar\n---\nbar docs\n---\nbar comment") } @(test) @@ -2509,7 +2509,7 @@ ast_hover_struct_field_should_show_docs_and_comments_multipointers :: proc(t: ^t } `, } - test.expect_hover(t, &source, "Foo.bar: [^]int\n bar docs\n\n// bar comment") + test.expect_hover(t, &source, "Foo.bar: [^]int\n---\nbar docs\n---\nbar comment") } @(test) @@ -2528,7 +2528,7 @@ ast_hover_struct_field_should_show_docs_and_comments_dynamic_arrays :: proc(t: ^ } `, } - test.expect_hover(t, &source, "Foo.bar: [dynamic]int\n bar docs\n\n// bar comment") + test.expect_hover(t, &source, "Foo.bar: [dynamic]int\n---\nbar docs\n---\nbar comment") } @(test) @@ -2547,7 +2547,7 @@ ast_hover_struct_field_should_show_docs_and_comments_fixed_arrays :: proc(t: ^te } `, } - test.expect_hover(t, &source, "Foo.bar: [5]int\n bar docs\n\n// bar comment") + test.expect_hover(t, &source, "Foo.bar: [5]int\n---\nbar docs\n---\nbar comment") } @(test) @@ -2566,7 +2566,7 @@ ast_hover_struct_field_should_show_docs_and_comments_matrix :: proc(t: ^testing. } `, } - test.expect_hover(t, &source, "Foo.bar: matrix[4,5]int\n bar docs\n\n// bar comment") + test.expect_hover(t, &source, "Foo.bar: matrix[4,5]int\n---\nbar docs\n---\nbar comment") } @(test) @@ -3191,7 +3191,7 @@ ast_hover_documentation_reexported :: proc(t: ^testing.T) { `, packages = packages[:], } - test.expect_hover(t, &source, "my_package.Foo :: struct{}\n Documentation for Foo") + test.expect_hover(t, &source, "my_package.Foo :: struct{}\n---\nDocumentation for Foo") } @(test) @@ -3217,7 +3217,7 @@ ast_hover_override_documentation_reexported :: proc(t: ^testing.T) { `, packages = packages[:], } - test.expect_hover(t, &source, "my_package.Foo :: struct{}\n New docs for Foo") + test.expect_hover(t, &source, "my_package.Foo :: struct{}\n---\nNew docs for Foo") } @(test) @@ -3495,7 +3495,7 @@ ast_hover_enum_field_directly :: proc(t: ^testing.T) { } `, } - test.expect_hover(t, &source, "test.Foo: .A\n Doc for A and B\n Mulitple lines!\n\n// comment for A and B") + test.expect_hover(t, &source, "test.Foo: .A\n---\nDoc for A and B\nMulitple lines!\n---\ncomment for A and B") } @(test) @@ -3624,7 +3624,7 @@ ast_hover_bit_set_intersection :: proc(t: ^testing.T) { foo_{*}b := foo_bar & {.Foo} // hover for foo_b `, } - test.expect_hover(t, &source, "test.foo_b: distinct bit_set[Flag]\n// hover for foo_b") + test.expect_hover(t, &source, "test.foo_b: distinct bit_set[Flag]\n---\nhover for foo_b") } @(test) @@ -3638,7 +3638,7 @@ ast_hover_bit_set_union :: proc(t: ^testing.T) { foo_{*}b := {.Foo} | foo_bar // hover for foo_b `, } - test.expect_hover(t, &source, "test.foo_b: distinct bit_set[Flag]\n// hover for foo_b") + test.expect_hover(t, &source, "test.foo_b: distinct bit_set[Flag]\n---\nhover for foo_b") } @(test) @@ -5366,7 +5366,7 @@ ast_hover_parapoly_other_package :: proc(t: ^testing.T) { `, packages = packages[:], } - test.expect_hover(t, &source, "my_package.bar :: proc(_: $T)\n Docs!\n\n// Comment!") + test.expect_hover(t, &source, "my_package.bar :: proc(_: $T)\n---\nDocs!\n---\nComment!") } @(test) @@ -5610,7 +5610,7 @@ ast_hover_local_proc_docs :: proc(t: ^testing.T) { } `, } - test.expect_hover(t, &source, "test.foo :: proc()\n foo doc") + test.expect_hover(t, &source, "test.foo :: proc()\n---\nfoo doc") } @(test) @@ -5785,7 +5785,7 @@ ast_hover_nested_proc_docs_tabs :: proc(t: ^testing.T) { } `, } - test.expect_hover(t, &source, "test.foo :: proc()\n\nDocs!\n\tDocs2\n") + test.expect_hover(t, &source, "test.foo :: proc()\n---\nDocs!\n\tDocs2\n") } @(test) @@ -5802,7 +5802,7 @@ ast_hover_nested_proc_docs_spaces :: proc(t: ^testing.T) { } `, } - test.expect_hover(t, &source, "test.foo :: proc()\n\nDocs!\n Docs2\n") + test.expect_hover(t, &source, "test.foo :: proc()\n---\nDocs!\n Docs2\n") } @(test) @@ -5825,7 +5825,7 @@ ast_hover_propagate_docs_alias_in_package :: proc(t: ^testing.T) { `, packages = packages[:], } - test.expect_hover(t, &source, "my_package.bar :: proc()\n Docs!\n\n// Comment!") + test.expect_hover(t, &source, "my_package.bar :: proc()\n---\nDocs!\n---\nComment!") } @(test) @@ -5849,7 +5849,333 @@ ast_hover_propagate_docs_alias_in_package_override :: proc(t: ^testing.T) { `, packages = packages[:], } - test.expect_hover(t, &source, "my_package.bar :: proc()\n Overridden\n\n// Comment!") + test.expect_hover(t, &source, "my_package.bar :: proc()\n---\nOverridden\n---\nComment!") +} + +@(test) +ast_hover_deferred_attributes :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + foo :: proc() {} + + @(deferred_in = fo{*}o) + bar :: proc() {} + `, + } + test.expect_hover(t, &source, "test.foo :: proc()") +} + +@(test) +ast_hover_const_aliases :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + Foo :: 3 + 4 + B{*}ar :: Foo + `, + } + test.expect_hover(t, &source, "test.Bar :: Foo") +} + +@(test) +ast_hover_const_aliases_from_other_pkg :: proc(t: ^testing.T) { + packages := make([dynamic]test.Package, context.temp_allocator) + + append(&packages, test.Package{pkg = "my_package", source = `package my_package + Foo :: 3 + 4 + `}) + source := test.Source { + main = `package test + import "my_package" + + B{*}ar :: my_package.Foo + `, + packages = packages[:], + } + test.expect_hover(t, &source, "test.Bar :: my_package.Foo") +} + +@(test) +ast_hover_directives_config_local :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + foo :: proc() { + b{*}ar := #config(TEST, false) + } + `, + } + test.expect_hover(t, &source, "test.bar: bool") +} + +@(test) +ast_hover_directives_load_type_local :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + foo :: proc() { + b{*}ar := #load("foo", string) + } + `, + } + test.expect_hover(t, &source, "test.bar: string") +} + +@(test) +ast_hover_directives_load_hash_local :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + + foo :: proc() { + b{*}ar := #load_hash("a", "b") + } + `, + } + test.expect_hover(t, &source, "test.bar: int") +} + +@(test) +ast_hover_directives_config :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + b{*}ar :: #config(TEST, false) + `, + } + test.expect_hover(t, &source, "test.bar :: #config(TEST, false)") +} + +@(test) +ast_hover_directives_load :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + b{*}ar :: #load("foo.txt") + `, + } + test.expect_hover(t, &source, "test.bar :: #load(\"foo.txt\")") +} + +@(test) +ast_hover_directives_config_info :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + bar :: #c{*}onfig(TEST, false) + `, + } + test.expect_hover(t, &source, "#config(<identifier>, default)\n\nChecks if an identifier is defined through the command line, or gives a default value instead.\n\nValues can be set with the `-define:NAME=VALUE` command line flag.") +} + +@(test) +ast_hover_proc_group_bitset :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + Foo :: enum { + A, + B, + } + + Foos :: bit_set[Foo] + + foo_one :: proc(i: int, foos: Foos) {} + foo_two :: proc(s: string, foos: Foos) {} + foo :: proc { + foo_one, + foo_two, + } + + main :: proc() { + foo(1, {.A{*}}) + } + `, + } + test.expect_hover(t, &source, "test.Foo: .A") +} + +@(test) +ast_hover_soa_struct_field_indexed :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + Foo :: struct{} + + Bar :: struct { + foos: #soa[dynamic]Foo, + } + + bazz :: proc(bar: ^Bar, index: int) { + f{*}oo := &bar.foos[index] + } + `, + } + test.expect_hover(t, &source, "test.foo: #soa^#soa[dynamic]Foo") +} + +@(test) +ast_hover_proc_poly_params :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + foo :: proc($T{*}: int) {} + `, + } + test.expect_hover(t, &source, "test.$T: int") +} + +@(test) +ast_hover_proc_poly_params_where_clause :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + foo :: proc($T: int) where T{*} >= 0 {} + `, + } + test.expect_hover(t, &source, "test.$T: int") +} + +@(test) +ast_hover_constant_unary_expr :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + F{*}OO :: ~u32(0) + `, + } + test.expect_hover(t, &source, "test.FOO :: ~u32(0)") +} + +@(test) +ast_hover_union_multiple_poly :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + Foo :: struct($T: typeid) {} + Bar :: struct{} + + Bazz :: union($T: typeid) { + Foo(T), + Bar, + } + + main :: proc() { + T :: distinct int + bazz: Ba{*}zz(T) + } + `, + } + test.expect_hover(t, &source, "test.Bazz :: union(T) {\n\tFoo(T),\n\tBar,\n}") +} + +@(test) +ast_hover_poly_proc_passthrough :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + make :: proc() -> (int, bool) { + return 1, true + } + + confirm_bool_one :: #force_inline proc(v: $T, ok: $B) -> (T, bool) { + return v, bool(ok) + } + + main :: proc() { + v{*}alue, ok := confirm_bool_one(make()) + } + `, + } + test.expect_hover(t, &source, "test.value: int") +} + +@(test) +ast_hover_parapoly_overloaded_proc_with_bitfield :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + Entry :: struct($T, $H: typeid) { + handle: H, + } + + SmallHandle :: bit_field int { + valid: bool | 1, + generation: int | 7, + index: int | 24, + } + + make :: proc { + makeEntry, + } + + makeEntry :: proc($T: typeid/Entry($D, $H), handle: H) -> (entry: T) { + return + } + + main :: proc() { + e{*}ntry := make(Entry(int, SmallHandle), SmallHandle{}) + } + `, + } + test.expect_hover(t, &source, "test.entry: test.Entry(int, SmallHandle)") +} + +@(test) +ast_hover_package_docs :: proc(t: ^testing.T) { + packages := make([dynamic]test.Package, context.temp_allocator) + + append( + &packages, + test.Package { + pkg = "my_package", + source = `// Package docs + package my_package + Foo :: struct{} + `, + }, + ) + source := test.Source { + main = `package test + import "my_package" + main :: proc() { + foo := my_packa{*}ge.Foo{} + } + `, + packages = packages[:], + } + + test.expect_hover(t, &source, "my_package: package\n---\nPackage docs") +} + +@(test) +ast_hover_import_path_package_docs :: proc(t: ^testing.T) { + packages := make([dynamic]test.Package, context.temp_allocator) + + append( + &packages, + test.Package { + pkg = "my_package", + source = `// Package docs + package my_package + `, + }, + ) + source := test.Source { + main = `package test + import "my_packa{*}ge" + `, + packages = packages[:], + } + + test.expect_hover(t, &source, "my_package: package\n---\nPackage docs") +} + +@(test) +ast_hover_proc_overload_generic_array_pointer_types :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + foo_dynamic_array :: proc(array: $A/[dynamic]^$T) {} + foo_slice :: proc(array: $A/[]^$T) {} + + foo :: proc{ + foo_dynamic_array, + foo_slice, + } + + main :: proc() { + array: [dynamic]^int + f{*}oo(array) + } + `, + } + + test.expect_hover(t, &source, "test.foo :: proc(array: $A/[dynamic]^$T)") } /* diff --git a/tests/references_test.odin b/tests/references_test.odin index 021033b..7bd2dcc 100644 --- a/tests/references_test.odin +++ b/tests/references_test.odin @@ -1520,3 +1520,21 @@ ast_references_enum_with_enumerated_array :: proc(t: ^testing.T) { test.expect_reference_locations(t, &source, locations[:]) } + +@(test) +ast_references_deferred_attributes :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + foo :: proc() {} + + @(deferred_in = fo{*}o) + bar :: proc() {} + `, + } + locations := []common.Location { + {range = {start = {line = 1, character = 2}, end = {line = 1, character = 5}}}, + {range = {start = {line = 3, character = 18}, end = {line = 3, character = 21}}}, + } + + test.expect_reference_locations(t, &source, locations[:]) +} diff --git a/tests/signatures_test.odin b/tests/signatures_test.odin index 1485731..4ee9ff3 100644 --- a/tests/signatures_test.odin +++ b/tests/signatures_test.odin @@ -1,6 +1,5 @@ package tests -import "core:fmt" import "core:testing" import test "src:testing" @@ -562,6 +561,132 @@ proc_signature_move_outside :: proc(t: ^testing.T) { ) } +@(test) +signature_comp_lit_struct :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + Foo :: struct { + a: int, + b: string, + } + + main :: proc() { + foo := Foo{ + {*} + } + } + `, + packages = {}, + config = { + enable_comp_lit_signature_help = true, + } + } + + test.expect_signature_labels( + t, + &source, + {"test.Foo :: struct {\n\ta: int,\n\tb: string,\n}"}, + ) +} + +@(test) +signature_comp_lit_struct_pre_declared :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + Foo :: struct { + a: int, // comment for a + b: string, + } + + main :: proc() { + foo: Foo + foo = { + {*} + } + } + `, + packages = {}, + config = { + enable_comp_lit_signature_help = true, + } + } + + test.expect_signature_labels( + t, + &source, + {"test.Foo :: struct {\n\ta: int, // comment for a\n\tb: string,\n}"}, + ) +} + +@(test) +signature_comp_lit_bit_set :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + Foo :: struct { + A, + B, + } + + Bar :: bit_set[Foo] + + main :: proc() { + bar: Bar + bar = { + {*} + } + } + `, + packages = {}, + config = { + enable_comp_lit_signature_help = true, + } + } + + test.expect_signature_labels( + t, + &source, + {"test.Bar :: bit_set[Foo]"}, + ) +} + +@(test) +signature_comp_lit_struct_field_after_comma :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + Foo :: struct { + A,{*} + B, + } + `, + config = { + enable_comp_lit_signature_help = true, + } + } + + test.expect_signature_labels( + t, + &source, + {}, + ) +} + +@(test) +signature_comp_lit_proc_field_after_comma :: proc(t: ^testing.T) { + source := test.Source { + main = `package test + foo :: proc(a, b,{*}: int) {} + `, + config = { + enable_comp_lit_signature_help = true, + } + } + + test.expect_signature_labels( + t, + &source, + {}, + ) +} /* @(test) signature_function_inside_when :: proc(t: ^testing.T) { diff --git a/tools/odinfmt/main.odin b/tools/odinfmt/main.odin index 3470ca5..69ad5cf 100644 --- a/tools/odinfmt/main.odin +++ b/tools/odinfmt/main.odin @@ -1,11 +1,9 @@ package odinfmt -import "core:encoding/json" import "core:flags" import "core:fmt" -import "core:io" import "core:mem" -import "core:odin/tokenizer" +import vmem "core:mem/virtual" import "core:os" import "core:path/filepath" import "core:strings" @@ -14,40 +12,32 @@ import "src:odin/format" import "src:odin/printer" Args :: struct { - write: bool `args:"name=w" usage:"write the new format to file"`, - stdin: bool `usage:"formats code from standard input"`, - path: string `args:"pos=0" usage:"set the file or directory to format"`, + write: bool `args:"name=w" usage:"write the new format to file"`, + stdin: bool `usage:"formats code from standard input"`, + path: string `args:"pos=0" usage:"set the file or directory to format"`, + config: string `usage:"path to a config file"`, } -format_file :: proc(filepath: string, config: printer.Config, allocator := context.allocator) -> (string, bool) { - if data, ok := os.read_entire_file(filepath, allocator); ok { +format_file :: proc( + filepath: string, + config: printer.Config, + allocator := context.allocator, +) -> ( + string, + bool, +) { + if data, err := os.read_entire_file(filepath, allocator); err == nil { return format.format(filepath, string(data), config, {.Optional_Semicolons}, allocator) } else { return "", false } } -files: [dynamic]string - -walk_files :: proc(info: os.File_Info, in_err: os.Errno, user_data: rawptr) -> (err: os.Error, skip_dir: bool) { - if info.is_dir { - return nil, false - } - - if filepath.ext(info.name) != ".odin" { - return nil, false - } - - append(&files, strings.clone(info.fullpath)) - - return nil, false -} - main :: proc() { - arena: mem.Arena - mem.arena_init(&arena, make([]byte, 50 * mem.Megabyte)) - - arena_allocator := mem.arena_allocator(&arena) + arena: vmem.Arena + arena_err := vmem.arena_init_growing(&arena) + ensure(arena_err == nil) + arena_allocator := vmem.arena_allocator(&arena) init_global_temporary_allocator(mem.Megabyte * 20) //enough space for the walk @@ -61,7 +51,7 @@ main :: proc() { args.path = "." } else { fmt.fprint(os.stderr, "Missing path to format\n") - flags.write_usage(os.stream_from_handle(os.stderr), Args, os.args[0]) + flags.write_usage(os.to_stream(os.stderr), Args, os.args[0]) os.exit(1) } } @@ -70,9 +60,14 @@ main :: proc() { write_failure := false - watermark := 0 + watermark: uint = 0 - config := format.find_config_file_or_default(args.path) + config: printer.Config + if args.config == "" { + config = format.find_config_file_or_default(args.path) + } else { + config = format.read_config_file_from_path_or_default(args.config) + } if args.stdin { data := make([dynamic]byte, arena_allocator) @@ -86,7 +81,13 @@ main :: proc() { append(&data, ..tmp[:r]) } - source, ok := format.format("<stdin>", string(data[:]), config, {.Optional_Semicolons}, arena_allocator) + source, ok := format.format( + "<stdin>", + string(data[:]), + config, + {.Optional_Semicolons}, + arena_allocator, + ) if ok { fmt.println(source) @@ -101,7 +102,7 @@ main :: proc() { if data, ok := format_file(args.path, config, arena_allocator); ok { os.rename(args.path, backup_path) - if os.write_entire_file(args.path, transmute([]byte)data) { + if err := os.write_entire_file(args.path, transmute([]byte)data); err == nil { os.remove(backup_path) } } else { @@ -114,7 +115,20 @@ main :: proc() { } } } else if os.is_dir(args.path) { - filepath.walk(args.path, walk_files, nil) + files: [dynamic]string + w := os.walker_create(args.path) + defer os.walker_destroy(&w) + for info in os.walker_walk(&w) { + if info.type == .Directory { + continue + } + + if filepath.ext(info.name) != ".odin" { + continue + } + + append(&files, strings.clone(info.fullpath)) + } for file in files { fmt.println(file) @@ -126,7 +140,7 @@ main :: proc() { if args.write { os.rename(file, backup_path) - if os.write_entire_file(file, transmute([]byte)data) { + if err := os.write_entire_file(file, transmute([]byte)data); err == nil { os.remove(backup_path) } } else { @@ -137,7 +151,7 @@ main :: proc() { write_failure = true } - watermark = max(watermark, arena.offset) + watermark = max(watermark, arena.total_used) free_all(arena_allocator) } diff --git a/tools/odinfmt/snapshot/snapshot.odin b/tools/odinfmt/snapshot/snapshot.odin index 3220845..c51c216 100644 --- a/tools/odinfmt/snapshot/snapshot.odin +++ b/tools/odinfmt/snapshot/snapshot.odin @@ -5,14 +5,13 @@ import "core:fmt" import "core:os" import "core:path/filepath" import "core:strings" -import "core:testing" import "core:text/scanner" import "src:odin/format" import "src:odin/printer" format_file :: proc(filepath: string, allocator := context.allocator) -> (string, bool) { - if data, ok := os.read_entire_file(filepath, allocator); ok { + if data, err := os.read_entire_file(filepath, allocator); err == nil { config := read_config_file_or_default(filepath) return format.format(filepath, string(data), config, {.Optional_Semicolons}, allocator) } else { @@ -30,7 +29,7 @@ read_config_file_or_default :: proc(fullpath: string, allocator := context.alloc if (os.exists(configpath)) { json_config := default_style - if data, ok := os.read_entire_file(configpath, allocator); ok { + if data, err := os.read_entire_file(configpath, allocator); err == nil { if json.unmarshal(data, &json_config) == nil { return json_config } @@ -44,7 +43,7 @@ read_config_file_or_default :: proc(fullpath: string, allocator := context.alloc snapshot_directory :: proc(directory: string) -> bool { matches, err := filepath.glob(fmt.tprintf("%v/*", directory)) - if err != .None { + if err != nil { fmt.eprintf("Error in globbing directory: %v", directory) } @@ -69,7 +68,7 @@ snapshot_file :: proc(path: string) -> bool { fmt.printf("Testing snapshot %v", path) - snapshot_path := filepath.join( + snapshot_path, _ := filepath.join( elems = {filepath.dir(path, context.temp_allocator), "/.snapshots", filepath.base(path)}, allocator = context.temp_allocator, ) @@ -82,7 +81,7 @@ snapshot_file :: proc(path: string) -> bool { } if os.exists(snapshot_path) { - if snapshot_data, ok := os.read_entire_file(snapshot_path, context.temp_allocator); ok { + if snapshot_data, err := os.read_entire_file(snapshot_path, context.temp_allocator); err == nil { snapshot_scanner := scanner.Scanner{} scanner.init(&snapshot_scanner, string(snapshot_data)) formatted_scanner := scanner.Scanner{} @@ -107,7 +106,7 @@ snapshot_file :: proc(path: string) -> bool { if s_ch != f_ch { fmt.eprintf("\nFormatted file was different from snapshot file: %v\n", snapshot_path) - os.write_entire_file(fmt.tprintf("%v_failed", snapshot_path), transmute([]u8)formatted) + _ = os.write_entire_file(fmt.tprintf("%v_failed", snapshot_path), transmute([]u8)formatted) return false } } @@ -118,9 +117,8 @@ snapshot_file :: proc(path: string) -> bool { } } else { os.make_directory(filepath.dir(snapshot_path, context.temp_allocator)) - ok = os.write_entire_file(snapshot_path, transmute([]byte)formatted) - if !ok { - fmt.eprintf("Failed to write snapshot file %v", snapshot_path) + if err := os.write_entire_file(snapshot_path, transmute([]byte)formatted); err != nil { + fmt.eprintf("Failed to write snapshot file %v: %v", snapshot_path, err) return false } } diff --git a/tools/odinfmt/tests.odin b/tools/odinfmt/tests.odin index 313e33c..0917838 100644 --- a/tools/odinfmt/tests.odin +++ b/tools/odinfmt/tests.odin @@ -1,8 +1,6 @@ package odinfmt_tests -import "core:testing" import "core:os" -import "core:fmt" import "core:mem" import "snapshot" |