From 40706139690b2eac3d5321e57600d0f414cd179b Mon Sep 17 00:00:00 2001 From: Nathaniel Saxe Date: Thu, 1 Jan 2026 20:48:59 -0500 Subject: populate remaining switch cases action for enums --- src/server/action.odin | 126 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) (limited to 'src') diff --git a/src/server/action.odin b/src/server/action.odin index d1608b5..e174f7e 100644 --- a/src/server/action.odin +++ b/src/server/action.odin @@ -4,6 +4,7 @@ import "core:fmt" import "core:log" import "core:odin/ast" import path "core:path/slashpath" +import "core:slice" import "core:strings" import "src:common" @@ -71,6 +72,15 @@ 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( + document, + &ast_context, + &position_context, + strings.clone(document.uri.uri), + &actions, + ) + } return actions[:], true } @@ -182,3 +192,119 @@ add_missing_imports :: proc( return } + +get_block_original_text :: proc(block: []^ast.Stmt, document_text: []u8) -> string { + if len(block) == 0 { + return "" + } + start := block[0].pos + end := block[max(0, len(block) - 1)].end + return string(document_text[start.offset:end.offset]) +} + +get_switch_cases_info :: proc( + document: ^Document, + ast_context: ^AstContext, + position_context: ^DocumentPositionContext, +) -> ( + existing_cases: map[string]string, + enum_names: []string, + ok: bool, +) { + if switch_block, ok := position_context.switch_stmt.body.derived.(^ast.Block_Stmt); ok { + existing_cases = make(map[string]string, 5, context.temp_allocator) + for stmt in switch_block.stmts { + if case_clause, ok := stmt.derived.(^ast.Case_Clause); ok { + case_name := "" + for name in case_clause.list { + if implicit, ok := name.derived.(^ast.Implicit_Selector_Expr); ok { + case_name = implicit.field.name + break + } + } + if case_name != "" { + existing_cases[case_name] = get_block_original_text(case_clause.body, document.text) + } + } + } + } + enum_value, _, unwrap_ok := unwrap_enum(ast_context, position_context.switch_stmt.cond) + if !unwrap_ok {return nil, nil, false} + return existing_cases, enum_value.names, true +} + +create_populate_switch_cases_edit :: proc( + position_context: ^DocumentPositionContext, + existing_cases: map[string]string, + enum_names: []string, +) -> ( + 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 + } + //entirety of the switch block + range := common.get_token_range(position_context.switch_stmt.body.stmt_base, position_context.file.src) + replacement_builder := strings.builder_make() + b := &replacement_builder + fmt.sbprintln(b, "{") + for name in enum_names { + fmt.sbprintln(b, "case .", name, ":", sep = "") + if name in existing_cases { + case_block := existing_cases[name] + if case_block != "" { + fmt.sbprintln(b, existing_cases[name]) + } + } + } + for name in existing_cases { + if !slice.contains(enum_names, name) { + //this case probably shouldn't exist + //since it's not one of the legal enum names, + //but don't delete the user's code inside the block + fmt.sbprintln(b, "case .", name, ":", sep = "") + case_block := existing_cases[name] + if case_block != "" { + fmt.sbprintln(b, existing_cases[name]) + } + } + } + fmt.sbprint(b, "}") + return TextEdit{range = range, newText = strings.to_string(replacement_builder)}, true +} +add_populate_switch_cases_action :: proc( + document: ^Document, + ast_context: ^AstContext, + position_context: ^DocumentPositionContext, + uri: string, + actions: ^[dynamic]CodeAction, +) { + existing_cases, enum_names, ok := get_switch_cases_info(document, ast_context, position_context) + if !ok {return} + all_cases_covered := true + for name in enum_names { + if name not_in existing_cases { + all_cases_covered = false + } + } + if all_cases_covered {return} //action not needed + edit, edit_ok := create_populate_switch_cases_edit(position_context, existing_cases, enum_names) + 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, + }, + ) +} -- cgit v1.2.3 From 0ad323e79c56a41e0e4818c646088cce983fade4 Mon Sep 17 00:00:00 2001 From: Nathaniel Saxe Date: Fri, 2 Jan 2026 12:57:54 -0500 Subject: make it work for enum switch statements as well --- src/server/action.odin | 91 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 66 insertions(+), 25 deletions(-) (limited to 'src') diff --git a/src/server/action.odin b/src/server/action.odin index e174f7e..72ef888 100644 --- a/src/server/action.odin +++ b/src/server/action.odin @@ -208,35 +208,71 @@ get_switch_cases_info :: proc( position_context: ^DocumentPositionContext, ) -> ( existing_cases: map[string]string, - enum_names: []string, + all_case_names: []string, + is_enum: bool, ok: bool, ) { - if switch_block, ok := position_context.switch_stmt.body.derived.(^ast.Block_Stmt); ok { - existing_cases = make(map[string]string, 5, context.temp_allocator) - for stmt in switch_block.stmts { - if case_clause, ok := stmt.derived.(^ast.Case_Clause); ok { - case_name := "" - for name in case_clause.list { + log.error(position_context.switch_stmt, position_context.switch_type_stmt) + if (position_context.switch_stmt == nil && position_context.switch_type_stmt == nil) || + (position_context.switch_stmt != nil && position_context.switch_stmt.cond == nil) { + return nil, nil, false, false + } + switch_block: ^ast.Block_Stmt + found_switch_block: bool + if position_context.switch_stmt != nil { + switch_block, found_switch_block = position_context.switch_stmt.body.derived.(^ast.Block_Stmt) + is_enum = true + } + 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) + } + if !found_switch_block { + return nil, nil, false, false + } + existing_cases = make(map[string]string, 5, context.temp_allocator) + for stmt in switch_block.stmts { + if case_clause, ok := stmt.derived.(^ast.Case_Clause); ok { + case_name := "" + for name in case_clause.list { + if is_enum { if implicit, ok := name.derived.(^ast.Implicit_Selector_Expr); ok { case_name = implicit.field.name break } + } else { + if ident, ok := name.derived.(^ast.Ident); ok { + case_name = ident.name + break + } } - if case_name != "" { - existing_cases[case_name] = get_block_original_text(case_clause.body, document.text) - } + } + if case_name != "" { + existing_cases[case_name] = get_block_original_text(case_clause.body, document.text) } } } - enum_value, _, unwrap_ok := unwrap_enum(ast_context, position_context.switch_stmt.cond) - if !unwrap_ok {return nil, nil, false} - return existing_cases, enum_value.names, true + log.error(existing_cases) + if is_enum { + enum_value, was_super_enum, unwrap_ok := unwrap_enum(ast_context, position_context.switch_stmt.cond) + if !unwrap_ok {return nil, nil, true, false} + return existing_cases, enum_value.names, !was_super_enum, true + } else { + st := position_context.switch_type_stmt + union_value, unwrap_ok := unwrap_union(ast_context, st.tag.derived.(^ast.Assign_Stmt).rhs[0]) + if !unwrap_ok {return nil, nil, false, false} + case_names := make([]string, len(union_value.types), context.temp_allocator) + for t, i in union_value.types { + case_names[i] = t.derived.(^ast.Ident).name + } + return existing_cases, case_names, false, true + } } create_populate_switch_cases_edit :: proc( position_context: ^DocumentPositionContext, existing_cases: map[string]string, - enum_names: []string, + all_case_names: []string, + is_enum: bool, ) -> ( TextEdit, bool, @@ -246,12 +282,18 @@ create_populate_switch_cases_edit :: proc( return {}, false } //entirety of the switch block - range := common.get_token_range(position_context.switch_stmt.body.stmt_base, position_context.file.src) + range: common.Range + if is_enum { + range = common.get_token_range(position_context.switch_stmt.body.stmt_base, position_context.file.src) + } else { + range = common.get_token_range(position_context.switch_type_stmt.body.stmt_base, position_context.file.src) + } replacement_builder := strings.builder_make() + dot := is_enum ? "." : "" b := &replacement_builder fmt.sbprintln(b, "{") - for name in enum_names { - fmt.sbprintln(b, "case .", name, ":", sep = "") + for name in all_case_names { + fmt.sbprintln(b, "case ", dot, name, ":", sep = "") if name in existing_cases { case_block := existing_cases[name] if case_block != "" { @@ -260,11 +302,10 @@ create_populate_switch_cases_edit :: proc( } } for name in existing_cases { - if !slice.contains(enum_names, name) { - //this case probably shouldn't exist - //since it's not one of the legal enum names, - //but don't delete the user's code inside the block - fmt.sbprintln(b, "case .", name, ":", sep = "") + if !slice.contains(all_case_names, name) { + //this case probably should be deleted by the user since it's not one of the legal enum names, + //but we shouldn't preemptively delete the user's code inside the block + fmt.sbprintln(b, "case ", dot, name, ":", sep = "") case_block := existing_cases[name] if case_block != "" { fmt.sbprintln(b, existing_cases[name]) @@ -281,16 +322,16 @@ add_populate_switch_cases_action :: proc( uri: string, actions: ^[dynamic]CodeAction, ) { - existing_cases, enum_names, ok := get_switch_cases_info(document, ast_context, position_context) + existing_cases, all_case_names, is_enum, ok := get_switch_cases_info(document, ast_context, position_context) if !ok {return} all_cases_covered := true - for name in enum_names { + for name in all_case_names { if name not_in existing_cases { all_cases_covered = false } } if all_cases_covered {return} //action not needed - edit, edit_ok := create_populate_switch_cases_edit(position_context, existing_cases, enum_names) + edit, edit_ok := create_populate_switch_cases_edit(position_context, existing_cases, all_case_names, is_enum) if !edit_ok {return} textEdits := make([dynamic]TextEdit, context.temp_allocator) append(&textEdits, edit) -- cgit v1.2.3 From eea11e9d1e3035b89351cd888b35b920a40de81b Mon Sep 17 00:00:00 2001 From: Nathaniel Saxe Date: Fri, 2 Jan 2026 12:59:09 -0500 Subject: rm logs --- src/server/action.odin | 2 -- 1 file changed, 2 deletions(-) (limited to 'src') diff --git a/src/server/action.odin b/src/server/action.odin index 72ef888..2099950 100644 --- a/src/server/action.odin +++ b/src/server/action.odin @@ -212,7 +212,6 @@ get_switch_cases_info :: proc( is_enum: bool, ok: bool, ) { - log.error(position_context.switch_stmt, position_context.switch_type_stmt) if (position_context.switch_stmt == nil && position_context.switch_type_stmt == nil) || (position_context.switch_stmt != nil && position_context.switch_stmt.cond == nil) { return nil, nil, false, false @@ -251,7 +250,6 @@ get_switch_cases_info :: proc( } } } - log.error(existing_cases) if is_enum { enum_value, was_super_enum, unwrap_ok := unwrap_enum(ast_context, position_context.switch_stmt.cond) if !unwrap_ok {return nil, nil, true, false} -- cgit v1.2.3 From dc0196b01092768689e2bc943a011440ae99119a Mon Sep 17 00:00:00 2001 From: Nathaniel Saxe Date: Fri, 30 Jan 2026 00:39:02 -0500 Subject: more or less handle union types - TODO in appropriate places on why not completely handled --- src/server/action.odin | 14 +++++++++++--- src/server/completion.odin | 3 ++- 2 files changed, 13 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/server/action.odin b/src/server/action.odin index 2099950..d5b4a90 100644 --- a/src/server/action.odin +++ b/src/server/action.odin @@ -239,8 +239,10 @@ get_switch_cases_info :: proc( break } } else { - if ident, ok := name.derived.(^ast.Ident); ok { - case_name = ident.name + if ty, ok := resolve_type_expression(ast_context, name); ok { + //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 + case_name = ty.name != "" ? ty.name : get_signature(ast_context, ty) break } } @@ -260,7 +262,13 @@ get_switch_cases_info :: proc( if !unwrap_ok {return nil, nil, false, false} case_names := make([]string, len(union_value.types), context.temp_allocator) for t, i in union_value.types { - case_names[i] = t.derived.(^ast.Ident).name + if ty, ok := resolve_type_expression(ast_context, t); ok { + //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 + case_names[i] = ty.name != "" ? ty.name : get_signature(ast_context, ty) + } else { + case_names[i] = "invalid type expression" + } } return existing_cases, case_names, false, true } diff --git a/src/server/completion.odin b/src/server/completion.odin index 31ecf0e..1fbae93 100644 --- a/src/server/completion.odin +++ b/src/server/completion.odin @@ -1974,7 +1974,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 -- cgit v1.2.3 From bb7e99dde0049e9e1ec3437f45a8b3ccfe98e959 Mon Sep 17 00:00:00 2001 From: Nathaniel Saxe Date: Sat, 31 Jan 2026 15:56:08 -0500 Subject: fix: reset ast context between resolve calls --- src/server/action.odin | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/server/action.odin b/src/server/action.odin index d5b4a90..58dc794 100644 --- a/src/server/action.odin +++ b/src/server/action.odin @@ -239,6 +239,7 @@ get_switch_cases_info :: proc( break } } else { + reset_ast_context(ast_context) if ty, ok := resolve_type_expression(ast_context, name); ok { //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 @@ -254,14 +255,23 @@ get_switch_cases_info :: proc( } if is_enum { enum_value, was_super_enum, unwrap_ok := unwrap_enum(ast_context, position_context.switch_stmt.cond) - if !unwrap_ok {return nil, nil, true, false} + if !unwrap_ok { + return nil, nil, true, false + } return existing_cases, enum_value.names, !was_super_enum, true } else { st := position_context.switch_type_stmt + if st == nil { + return nil, nil, false, 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 nil, nil, false, false} + if !unwrap_ok { + return nil, nil, false, false + } case_names := make([]string, len(union_value.types), context.temp_allocator) for t, i in union_value.types { + reset_ast_context(ast_context) if ty, ok := resolve_type_expression(ast_context, t); ok { //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 -- cgit v1.2.3 From eb78def62dbc65212fdb67d0b6e181fc15183c32 Mon Sep 17 00:00:00 2001 From: Nathaniel Saxe Date: Sat, 31 Jan 2026 20:57:53 -0500 Subject: private my helper functions so as not to pollute namespace --- src/server/action.odin | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src') diff --git a/src/server/action.odin b/src/server/action.odin index 58dc794..9402eea 100644 --- a/src/server/action.odin +++ b/src/server/action.odin @@ -193,6 +193,7 @@ add_missing_imports :: proc( return } +@(private = "file") get_block_original_text :: proc(block: []^ast.Stmt, document_text: []u8) -> string { if len(block) == 0 { return "" @@ -202,6 +203,7 @@ get_block_original_text :: proc(block: []^ast.Stmt, document_text: []u8) -> stri return string(document_text[start.offset:end.offset]) } +@(private = "file") get_switch_cases_info :: proc( document: ^Document, ast_context: ^AstContext, @@ -284,6 +286,7 @@ get_switch_cases_info :: proc( } } +@(private = "file") create_populate_switch_cases_edit :: proc( position_context: ^DocumentPositionContext, existing_cases: map[string]string, -- cgit v1.2.3 From 1dddd343a6e2a70cba078379dcfde0d62cd28a7c Mon Sep 17 00:00:00 2001 From: Nathaniel Saxe Date: Tue, 3 Feb 2026 13:35:38 -0500 Subject: first put existing cases in order, then put in new cases --- src/server/action.odin | 55 +++++++++++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 25 deletions(-) (limited to 'src') diff --git a/src/server/action.odin b/src/server/action.odin index 9402eea..f8f3f24 100644 --- a/src/server/action.odin +++ b/src/server/action.odin @@ -210,13 +210,14 @@ get_switch_cases_info :: proc( position_context: ^DocumentPositionContext, ) -> ( existing_cases: map[string]string, + existing_case_order: []string, all_case_names: []string, is_enum: bool, ok: bool, ) { if (position_context.switch_stmt == nil && position_context.switch_type_stmt == nil) || (position_context.switch_stmt != nil && position_context.switch_stmt.cond == nil) { - return nil, nil, false, false + return nil, nil, nil, false, false } switch_block: ^ast.Block_Stmt found_switch_block: bool @@ -228,9 +229,10 @@ get_switch_cases_info :: proc( switch_block, found_switch_block = position_context.switch_type_stmt.body.derived.(^ast.Block_Stmt) } if !found_switch_block { - return nil, nil, false, false + return nil, nil, nil, false, false } existing_cases = make(map[string]string, 5, context.temp_allocator) + existing_cases_in_order := make([dynamic]string, context.temp_allocator) for stmt in switch_block.stmts { if case_clause, ok := stmt.derived.(^ast.Case_Clause); ok { case_name := "" @@ -252,24 +254,25 @@ get_switch_cases_info :: proc( } if case_name != "" { existing_cases[case_name] = get_block_original_text(case_clause.body, document.text) + append(&existing_cases_in_order, case_name) } } } if is_enum { enum_value, was_super_enum, unwrap_ok := unwrap_enum(ast_context, position_context.switch_stmt.cond) if !unwrap_ok { - return nil, nil, true, false + return nil, nil, nil, true, false } - return existing_cases, enum_value.names, !was_super_enum, true + return existing_cases, existing_cases_in_order[:], enum_value.names, !was_super_enum, true } else { st := position_context.switch_type_stmt if st == nil { - return nil, nil, false, false + return nil, nil, nil, false, 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 nil, nil, false, false + return nil, nil, nil, false, false } case_names := make([]string, len(union_value.types), context.temp_allocator) for t, i in union_value.types { @@ -282,7 +285,7 @@ get_switch_cases_info :: proc( case_names[i] = "invalid type expression" } } - return existing_cases, case_names, false, true + return existing_cases, existing_cases_in_order[:], case_names, false, true } } @@ -290,6 +293,7 @@ get_switch_cases_info :: proc( create_populate_switch_cases_edit :: proc( position_context: ^DocumentPositionContext, existing_cases: map[string]string, + existing_case_order: []string, all_case_names: []string, is_enum: bool, ) -> ( @@ -311,25 +315,16 @@ create_populate_switch_cases_edit :: proc( dot := is_enum ? "." : "" b := &replacement_builder fmt.sbprintln(b, "{") - for name in all_case_names { + for name in existing_case_order { fmt.sbprintln(b, "case ", dot, name, ":", sep = "") - if name in existing_cases { - case_block := existing_cases[name] - if case_block != "" { - fmt.sbprintln(b, existing_cases[name]) - } + case_block, case_exists := existing_cases[name] + if case_exists && case_block != "" { + fmt.sbprintln(b, existing_cases[name]) } } - for name in existing_cases { - if !slice.contains(all_case_names, name) { - //this case probably should be deleted by the user since it's not one of the legal enum names, - //but we shouldn't preemptively delete the user's code inside the block - fmt.sbprintln(b, "case ", dot, name, ":", sep = "") - case_block := existing_cases[name] - if case_block != "" { - fmt.sbprintln(b, existing_cases[name]) - } - } + for name in all_case_names { + if name in existing_cases {continue} //covered by prev loop + fmt.sbprintln(b, "case ", dot, name, ":", sep = "") } fmt.sbprint(b, "}") return TextEdit{range = range, newText = strings.to_string(replacement_builder)}, true @@ -341,7 +336,11 @@ add_populate_switch_cases_action :: proc( uri: string, actions: ^[dynamic]CodeAction, ) { - existing_cases, all_case_names, is_enum, ok := get_switch_cases_info(document, ast_context, position_context) + existing_cases, existing_case_order, all_case_names, is_enum, ok := get_switch_cases_info( + document, + ast_context, + position_context, + ) if !ok {return} all_cases_covered := true for name in all_case_names { @@ -350,7 +349,13 @@ add_populate_switch_cases_action :: proc( } } if all_cases_covered {return} //action not needed - edit, edit_ok := create_populate_switch_cases_edit(position_context, existing_cases, all_case_names, is_enum) + edit, edit_ok := create_populate_switch_cases_edit( + position_context, + existing_cases, + existing_case_order, + all_case_names, + is_enum, + ) if !edit_ok {return} textEdits := make([dynamic]TextEdit, context.temp_allocator) append(&textEdits, edit) -- cgit v1.2.3 From e09faaa16c35721d37d61625ce14239ac29ba6eb Mon Sep 17 00:00:00 2001 From: Nathaniel Saxe Date: Tue, 3 Feb 2026 17:03:16 -0500 Subject: refactor into file; handle indentation --- src/server/action.odin | 182 ----------------------- src/server/action_invert_if_statements.odin | 19 ++- src/server/action_populate_switch_cases.odin | 206 +++++++++++++++++++++++++++ 3 files changed, 214 insertions(+), 193 deletions(-) create mode 100644 src/server/action_populate_switch_cases.odin (limited to 'src') diff --git a/src/server/action.odin b/src/server/action.odin index 43b38f3..64516a3 100644 --- a/src/server/action.odin +++ b/src/server/action.odin @@ -4,7 +4,6 @@ import "core:fmt" import "core:log" import "core:odin/ast" import path "core:path/slashpath" -import "core:slice" import "core:strings" import "src:common" @@ -194,184 +193,3 @@ add_missing_imports :: proc( return } - -@(private = "file") -get_block_original_text :: proc(block: []^ast.Stmt, document_text: []u8) -> string { - if len(block) == 0 { - return "" - } - start := block[0].pos - end := block[max(0, len(block) - 1)].end - return string(document_text[start.offset:end.offset]) -} - -@(private = "file") -get_switch_cases_info :: proc( - document: ^Document, - ast_context: ^AstContext, - position_context: ^DocumentPositionContext, -) -> ( - existing_cases: map[string]string, - existing_case_order: []string, - all_case_names: []string, - is_enum: bool, - ok: bool, -) { - if (position_context.switch_stmt == nil && position_context.switch_type_stmt == nil) || - (position_context.switch_stmt != nil && position_context.switch_stmt.cond == nil) { - return nil, nil, nil, false, false - } - switch_block: ^ast.Block_Stmt - found_switch_block: bool - if position_context.switch_stmt != nil { - switch_block, found_switch_block = position_context.switch_stmt.body.derived.(^ast.Block_Stmt) - is_enum = true - } - 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) - } - if !found_switch_block { - return nil, nil, nil, false, false - } - existing_cases = make(map[string]string, 5, context.temp_allocator) - existing_cases_in_order := make([dynamic]string, context.temp_allocator) - for stmt in switch_block.stmts { - if case_clause, ok := stmt.derived.(^ast.Case_Clause); ok { - case_name := "" - for name in case_clause.list { - if is_enum { - if implicit, ok := name.derived.(^ast.Implicit_Selector_Expr); ok { - case_name = implicit.field.name - break - } - } else { - reset_ast_context(ast_context) - if ty, ok := resolve_type_expression(ast_context, name); ok { - //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 - case_name = ty.name != "" ? ty.name : get_signature(ast_context, ty) - break - } - } - } - if case_name != "" { - existing_cases[case_name] = get_block_original_text(case_clause.body, document.text) - append(&existing_cases_in_order, case_name) - } - } - } - if is_enum { - enum_value, was_super_enum, unwrap_ok := unwrap_enum(ast_context, position_context.switch_stmt.cond) - if !unwrap_ok { - return nil, nil, nil, true, false - } - return existing_cases, existing_cases_in_order[:], enum_value.names, !was_super_enum, true - } else { - st := position_context.switch_type_stmt - if st == nil { - return nil, nil, nil, false, 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 nil, nil, nil, false, false - } - case_names := make([]string, len(union_value.types), context.temp_allocator) - for t, i in union_value.types { - reset_ast_context(ast_context) - if ty, ok := resolve_type_expression(ast_context, t); ok { - //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 - case_names[i] = ty.name != "" ? ty.name : get_signature(ast_context, ty) - } else { - case_names[i] = "invalid type expression" - } - } - return existing_cases, existing_cases_in_order[:], case_names, false, true - } -} - -@(private = "file") -create_populate_switch_cases_edit :: proc( - position_context: ^DocumentPositionContext, - existing_cases: map[string]string, - existing_case_order: []string, - all_case_names: []string, - is_enum: bool, -) -> ( - 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 - } - //entirety of the switch block - range: common.Range - if is_enum { - range = common.get_token_range(position_context.switch_stmt.body.stmt_base, position_context.file.src) - } else { - range = common.get_token_range(position_context.switch_type_stmt.body.stmt_base, position_context.file.src) - } - replacement_builder := strings.builder_make() - dot := is_enum ? "." : "" - b := &replacement_builder - fmt.sbprintln(b, "{") - for name in existing_case_order { - fmt.sbprintln(b, "case ", dot, name, ":", sep = "") - case_block, case_exists := existing_cases[name] - if case_exists && case_block != "" { - fmt.sbprintln(b, existing_cases[name]) - } - } - for name in all_case_names { - if name in existing_cases {continue} //covered by prev loop - fmt.sbprintln(b, "case ", dot, name, ":", sep = "") - } - fmt.sbprint(b, "}") - return TextEdit{range = range, newText = strings.to_string(replacement_builder)}, true -} -add_populate_switch_cases_action :: proc( - document: ^Document, - ast_context: ^AstContext, - position_context: ^DocumentPositionContext, - uri: string, - actions: ^[dynamic]CodeAction, -) { - existing_cases, existing_case_order, all_case_names, is_enum, ok := get_switch_cases_info( - document, - ast_context, - position_context, - ) - if !ok {return} - all_cases_covered := true - for name in all_case_names { - if name not_in existing_cases { - all_cases_covered = false - } - } - if all_cases_covered {return} //action not needed - edit, edit_ok := create_populate_switch_cases_edit( - position_context, - existing_cases, - existing_case_order, - all_case_names, - is_enum, - ) - 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/action_invert_if_statements.odin b/src/server/action_invert_if_statements.odin index 8e904e5..a41127a 100644 --- a/src/server/action_invert_if_statements.odin +++ b/src/server/action_invert_if_statements.odin @@ -19,7 +19,7 @@ import "src:common" * So for now, we only support only one level of if statements without else-if chains. */ -@(private="package") +@(private = "package") add_invert_if_action :: proc( document: ^Document, position: common.AbsolutePosition, @@ -45,15 +45,7 @@ add_invert_if_action :: proc( 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, - }, - ) + append(actions, CodeAction{kind = "refactor.more", isPreferred = false, title = "Invert if", edit = workspaceEdit}) } // Find the innermost if statement that contains the given position @@ -71,7 +63,11 @@ find_if_stmt_at_position :: proc(stmts: []^ast.Stmt, position: common.AbsolutePo return nil } -find_if_stmt_in_node :: proc(node: ^ast.Node, position: common.AbsolutePosition, in_else_clause: bool) -> ^ast.If_Stmt { +find_if_stmt_in_node :: proc( + node: ^ast.Node, + position: common.AbsolutePosition, + in_else_clause: bool, +) -> ^ast.If_Stmt { if node == nil { return nil } @@ -238,6 +234,7 @@ generate_inverted_if :: proc(document: ^Document, if_stmt: ^ast.If_Stmt) -> (str } // 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' { diff --git a/src/server/action_populate_switch_cases.odin b/src/server/action_populate_switch_cases.odin new file mode 100644 index 0000000..f2255c5 --- /dev/null +++ b/src/server/action_populate_switch_cases.odin @@ -0,0 +1,206 @@ +#+private file + +package server + +import "core:fmt" +import "core:odin/ast" +import "core:slice" +import "core:strings" + +import "src:common" + + +// Get the indentation (leading whitespace) 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]) +} + +SwitchCaseInfo :: struct { + name: string, + case_indentation: string, + body_indentation: string, + body: string, +} +get_switch_cases_info :: proc( + document: ^Document, + ast_context: ^AstContext, + position_context: ^DocumentPositionContext, +) -> ( + existing_cases: []SwitchCaseInfo, + all_case_names: []string, + is_enum: bool, + ok: bool, +) { + if (position_context.switch_stmt == nil && position_context.switch_type_stmt == nil) || + (position_context.switch_stmt != nil && position_context.switch_stmt.cond == nil) { + return nil, nil, false, false + } + switch_block: ^ast.Block_Stmt + found_switch_block: bool + if position_context.switch_stmt != nil { + switch_block, found_switch_block = position_context.switch_stmt.body.derived.(^ast.Block_Stmt) + is_enum = true + } + 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) + } + if !found_switch_block { + return nil, nil, false, false + } + existing_cases_in_order := make([dynamic]SwitchCaseInfo, context.temp_allocator) + for stmt in switch_block.stmts { + if case_clause, ok := stmt.derived.(^ast.Case_Clause); ok { + case_indent := get_line_indentation(string(document.text), case_clause.pos.offset) + case_name := "" + for name in case_clause.list { + if is_enum { + if implicit, ok := name.derived.(^ast.Implicit_Selector_Expr); ok { + case_name = implicit.field.name + break + } + } else { + reset_ast_context(ast_context) + if ty, ok := resolve_type_expression(ast_context, name); ok { + //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 + case_name = ty.name != "" ? ty.name : get_signature(ast_context, ty) + break + } + } + } + if case_name != "" { + case_info := SwitchCaseInfo { + name = case_name, + case_indentation = get_line_indentation(string(document.text), case_clause.pos.offset), + body = get_block_original_text(case_clause.body, string(document.text)), + } + append(&existing_cases_in_order, case_info) + } + } + } + if is_enum { + enum_value, was_super_enum, unwrap_ok := unwrap_enum(ast_context, position_context.switch_stmt.cond) + if !unwrap_ok { + return nil, nil, true, false + } + return existing_cases_in_order[:], enum_value.names, !was_super_enum, true + } else { + st := position_context.switch_type_stmt + if st == nil { + return nil, nil, false, 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 nil, nil, false, false + } + case_names := make([]string, len(union_value.types), context.temp_allocator) + for t, i in union_value.types { + reset_ast_context(ast_context) + if ty, ok := resolve_type_expression(ast_context, t); ok { + //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 + case_names[i] = ty.name != "" ? ty.name : get_signature(ast_context, ty) + } else { + case_names[i] = "invalid type expression" + } + } + return existing_cases_in_order[:], case_names, false, true + } +} + +create_populate_switch_cases_edit :: proc( + position_context: ^DocumentPositionContext, + existing_cases: []SwitchCaseInfo, + all_case_names: []string, + is_enum: bool, +) -> ( + 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 + } + //entirety of the switch block + range: common.Range + if is_enum { + range = common.get_token_range(position_context.switch_stmt.body.stmt_base, position_context.file.src) + } else { + range = common.get_token_range(position_context.switch_type_stmt.body.stmt_base, position_context.file.src) + } + replacement_builder := strings.builder_make() + dot := is_enum ? "." : "" + b := &replacement_builder + fmt.sbprintln(b, "{") + existing_cases_map := map[string]struct{}{} + indent_for_new_cases := "" + for case_info in existing_cases { + if indent_for_new_cases == "" {indent_for_new_cases = case_info.case_indentation} + existing_cases_map[case_info.name] = {} + fmt.sbprintln(b, case_info.case_indentation, "case ", dot, case_info.name, ":", sep = "") + case_body := case_info.body + if case_body != "" { + fmt.sbprintln(b, case_info.body) + } + } + for name in all_case_names { + if name in existing_cases_map {continue} //covered by prev loop + fmt.sbprintln(b, indent_for_new_cases, "case ", dot, name, ":", sep = "") + } + fmt.sbprint(b, indent_for_new_cases, "}") + return TextEdit{range = range, newText = strings.to_string(replacement_builder)}, true +} +@(private = "package") +add_populate_switch_cases_action :: proc( + document: ^Document, + ast_context: ^AstContext, + position_context: ^DocumentPositionContext, + uri: string, + actions: ^[dynamic]CodeAction, +) { + existing_cases, all_case_names, is_enum, ok := get_switch_cases_info(document, ast_context, position_context) + if !ok {return} + all_cases_covered := true + { + existing_cases_map := map[string]struct{}{} + for case_info in existing_cases { + existing_cases_map[case_info.name] = {} + } + for name in all_case_names { + if name not_in existing_cases_map { + all_cases_covered = false + } + } + } + if all_cases_covered {return} //action not needed + edit, edit_ok := create_populate_switch_cases_edit(position_context, existing_cases, all_case_names, is_enum) + 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, + }, + ) +} -- cgit v1.2.3 From 5c582738aa27ad8a6dd290be0a56ab6c619fd121 Mon Sep 17 00:00:00 2001 From: Nathaniel Saxe Date: Tue, 3 Feb 2026 17:05:05 -0500 Subject: sep='' --- src/server/action_populate_switch_cases.odin | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/server/action_populate_switch_cases.odin b/src/server/action_populate_switch_cases.odin index f2255c5..8489979 100644 --- a/src/server/action_populate_switch_cases.odin +++ b/src/server/action_populate_switch_cases.odin @@ -160,7 +160,7 @@ create_populate_switch_cases_edit :: proc( if name in existing_cases_map {continue} //covered by prev loop fmt.sbprintln(b, indent_for_new_cases, "case ", dot, name, ":", sep = "") } - fmt.sbprint(b, indent_for_new_cases, "}") + fmt.sbprint(b, indent_for_new_cases, "}", sep = "") return TextEdit{range = range, newText = strings.to_string(replacement_builder)}, true } @(private = "package") -- cgit v1.2.3 From 1d85ad0c195946e566eaac481fbb906b46e2aa4b Mon Sep 17 00:00:00 2001 From: Nathaniel Saxe Date: Tue, 3 Feb 2026 17:07:47 -0500 Subject: undo formatting changes in unrelated file --- src/server/action_invert_if_statements.odin | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/server/action_invert_if_statements.odin b/src/server/action_invert_if_statements.odin index a41127a..047b3a2 100644 --- a/src/server/action_invert_if_statements.odin +++ b/src/server/action_invert_if_statements.odin @@ -19,7 +19,7 @@ import "src:common" * So for now, we only support only one level of if statements without else-if chains. */ -@(private = "package") +@(private="package") add_invert_if_action :: proc( document: ^Document, position: common.AbsolutePosition, @@ -45,7 +45,15 @@ add_invert_if_action :: proc( 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}) + append( + actions, + CodeAction { + kind = "refactor.more", + isPreferred = false, + title = "Invert if", + edit = workspaceEdit, + }, + ) } // Find the innermost if statement that contains the given position @@ -63,11 +71,7 @@ find_if_stmt_at_position :: proc(stmts: []^ast.Stmt, position: common.AbsolutePo return nil } -find_if_stmt_in_node :: proc( - node: ^ast.Node, - position: common.AbsolutePosition, - in_else_clause: bool, -) -> ^ast.If_Stmt { +find_if_stmt_in_node :: proc(node: ^ast.Node, position: common.AbsolutePosition, in_else_clause: bool) -> ^ast.If_Stmt { if node == nil { return nil } @@ -234,7 +238,7 @@ generate_inverted_if :: proc(document: ^Document, if_stmt: ^ast.If_Stmt) -> (str } // Get the indentation (leading whitespace) of the line containing the given offset -@(private = "package") +@(private="package") get_line_indentation :: proc(src: string, offset: int) -> string { line_start := offset for line_start > 0 && src[line_start - 1] != '\n' { -- cgit v1.2.3 From 857cfd03a5d3b66402105fafe0ec262b4cae5186 Mon Sep 17 00:00:00 2001 From: Nathaniel Saxe Date: Tue, 3 Feb 2026 17:23:07 -0500 Subject: capture indentation of the overall switch block --- src/server/action_populate_switch_cases.odin | 59 ++++++++++++++++------------ 1 file changed, 33 insertions(+), 26 deletions(-) (limited to 'src') diff --git a/src/server/action_populate_switch_cases.odin b/src/server/action_populate_switch_cases.odin index 8489979..ca5c258 100644 --- a/src/server/action_populate_switch_cases.odin +++ b/src/server/action_populate_switch_cases.odin @@ -4,7 +4,6 @@ package server import "core:fmt" import "core:odin/ast" -import "core:slice" import "core:strings" import "src:common" @@ -29,7 +28,6 @@ get_block_original_text :: proc(block: []^ast.Stmt, document_text: string) -> st SwitchCaseInfo :: struct { name: string, - case_indentation: string, body_indentation: string, body: string, } @@ -40,12 +38,13 @@ get_switch_cases_info :: proc( ) -> ( existing_cases: []SwitchCaseInfo, all_case_names: []string, + switch_indentation: string, is_enum: bool, ok: bool, ) { if (position_context.switch_stmt == nil && position_context.switch_type_stmt == nil) || (position_context.switch_stmt != nil && position_context.switch_stmt.cond == nil) { - return nil, nil, false, false + return nil, nil, "", false, false } switch_block: ^ast.Block_Stmt found_switch_block: bool @@ -57,12 +56,12 @@ get_switch_cases_info :: proc( switch_block, found_switch_block = position_context.switch_type_stmt.body.derived.(^ast.Block_Stmt) } if !found_switch_block { - return nil, nil, false, false + return nil, nil, "", false, false } + switch_indentation = get_line_indentation(string(document.text), switch_block.pos.offset) existing_cases_in_order := make([dynamic]SwitchCaseInfo, context.temp_allocator) for stmt in switch_block.stmts { if case_clause, ok := stmt.derived.(^ast.Case_Clause); ok { - case_indent := get_line_indentation(string(document.text), case_clause.pos.offset) case_name := "" for name in case_clause.list { if is_enum { @@ -82,9 +81,8 @@ get_switch_cases_info :: proc( } if case_name != "" { case_info := SwitchCaseInfo { - name = case_name, - case_indentation = get_line_indentation(string(document.text), case_clause.pos.offset), - body = get_block_original_text(case_clause.body, string(document.text)), + name = case_name, + body = get_block_original_text(case_clause.body, string(document.text)), } append(&existing_cases_in_order, case_info) } @@ -93,18 +91,18 @@ get_switch_cases_info :: proc( if is_enum { enum_value, was_super_enum, unwrap_ok := unwrap_enum(ast_context, position_context.switch_stmt.cond) if !unwrap_ok { - return nil, nil, true, false + return nil, nil, "", true, false } - return existing_cases_in_order[:], enum_value.names, !was_super_enum, true + return existing_cases_in_order[:], enum_value.names, switch_indentation, !was_super_enum, true } else { st := position_context.switch_type_stmt if st == nil { - return nil, nil, false, false + return nil, nil, "", false, 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 nil, nil, false, false + return nil, nil, "", false, false } case_names := make([]string, len(union_value.types), context.temp_allocator) for t, i in union_value.types { @@ -117,13 +115,14 @@ get_switch_cases_info :: proc( case_names[i] = "invalid type expression" } } - return existing_cases_in_order[:], case_names, false, true + return existing_cases_in_order[:], case_names, switch_indentation, false, true } } create_populate_switch_cases_edit :: proc( position_context: ^DocumentPositionContext, existing_cases: []SwitchCaseInfo, + switch_indentation: string, all_case_names: []string, is_enum: bool, ) -> ( @@ -145,22 +144,20 @@ create_populate_switch_cases_edit :: proc( dot := is_enum ? "." : "" b := &replacement_builder fmt.sbprintln(b, "{") - existing_cases_map := map[string]struct{}{} - indent_for_new_cases := "" + existing_case_names := map[string]struct{}{} for case_info in existing_cases { - if indent_for_new_cases == "" {indent_for_new_cases = case_info.case_indentation} - existing_cases_map[case_info.name] = {} - fmt.sbprintln(b, case_info.case_indentation, "case ", dot, case_info.name, ":", sep = "") + existing_case_names[case_info.name] = {} + fmt.sbprintln(b, switch_indentation, "case ", dot, case_info.name, ":", sep = "") case_body := case_info.body if case_body != "" { fmt.sbprintln(b, case_info.body) } } for name in all_case_names { - if name in existing_cases_map {continue} //covered by prev loop - fmt.sbprintln(b, indent_for_new_cases, "case ", dot, name, ":", sep = "") + if name in existing_case_names {continue} //covered by prev loop + fmt.sbprintln(b, switch_indentation, "case ", dot, name, ":", sep = "") } - fmt.sbprint(b, indent_for_new_cases, "}", sep = "") + fmt.sbprint(b, switch_indentation, "}", sep = "") return TextEdit{range = range, newText = strings.to_string(replacement_builder)}, true } @(private = "package") @@ -171,22 +168,32 @@ add_populate_switch_cases_action :: proc( uri: string, actions: ^[dynamic]CodeAction, ) { - existing_cases, all_case_names, is_enum, ok := get_switch_cases_info(document, ast_context, position_context) + existing_cases, all_case_names, switch_indentation, is_enum, ok := get_switch_cases_info( + document, + ast_context, + position_context, + ) if !ok {return} all_cases_covered := true { - existing_cases_map := map[string]struct{}{} + existing_case_names := map[string]struct{}{} for case_info in existing_cases { - existing_cases_map[case_info.name] = {} + existing_case_names[case_info.name] = {} } for name in all_case_names { - if name not_in existing_cases_map { + if name not_in existing_case_names { all_cases_covered = false } } } if all_cases_covered {return} //action not needed - edit, edit_ok := create_populate_switch_cases_edit(position_context, existing_cases, all_case_names, is_enum) + edit, edit_ok := create_populate_switch_cases_edit( + position_context, + existing_cases, + switch_indentation, + all_case_names, + is_enum, + ) if !edit_ok {return} textEdits := make([dynamic]TextEdit, context.temp_allocator) append(&textEdits, edit) -- cgit v1.2.3 From 342a0e1a401627d76dbc20d22bf9d0788b3760c2 Mon Sep 17 00:00:00 2001 From: Nathaniel Saxe Date: Wed, 4 Feb 2026 15:35:08 -0500 Subject: qualify union case names with pointer / package when necessary --- src/server/action_populate_switch_cases.odin | 23 +++++++++++------- src/server/completion.odin | 35 +++++++++++++++++----------- 2 files changed, 37 insertions(+), 21 deletions(-) (limited to 'src') diff --git a/src/server/action_populate_switch_cases.odin b/src/server/action_populate_switch_cases.odin index ca5c258..a733aa5 100644 --- a/src/server/action_populate_switch_cases.odin +++ b/src/server/action_populate_switch_cases.odin @@ -9,7 +9,7 @@ import "core:strings" import "src:common" -// Get the indentation (leading whitespace) of the line containing the given offset +// 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' { @@ -63,18 +63,21 @@ get_switch_cases_info :: proc( for stmt in switch_block.stmts { if case_clause, ok := stmt.derived.(^ast.Case_Clause); ok { case_name := "" - for name in case_clause.list { + for clause in case_clause.list { if is_enum { - if implicit, ok := name.derived.(^ast.Implicit_Selector_Expr); ok { - case_name = implicit.field.name + if name, ok := get_used_switch_name(clause); ok { + case_name = name break } } else { reset_ast_context(ast_context) - if ty, ok := resolve_type_expression(ast_context, name); ok { + if symbol, ok := resolve_type_expression(ast_context, clause); 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 - case_name = ty.name != "" ? ty.name : get_signature(ast_context, ty) + if case_name == "" { + case_name = get_signature(ast_context, symbol) + } break } } @@ -107,10 +110,14 @@ get_switch_cases_info :: proc( case_names := make([]string, len(union_value.types), context.temp_allocator) for t, i in union_value.types { reset_ast_context(ast_context) - if ty, ok := resolve_type_expression(ast_context, t); ok { + 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 - case_names[i] = ty.name != "" ? ty.name : get_signature(ast_context, ty) + if case_name == "" { + case_name = get_signature(ast_context, symbol) + } + case_names[i] = case_name } else { case_names[i] = "invalid type expression" } diff --git a/src/server/completion.odin b/src/server/completion.odin index 8be6079..a7b1720 100644 --- a/src/server/completion.odin +++ b/src/server/completion.odin @@ -1943,12 +1943,32 @@ 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 { + if symbol.pkg == ast_context.document_package { + return fmt.aprintf("%v%v", repeat("^", symbol.pointers, context.temp_allocator), symbol.name) + } else { + return fmt.aprintf( + "%v%v.%v", + repeat("^", symbol.pointers, context.temp_allocator), + get_symbol_pkg_name(ast_context, symbol), + symbol.name, + ) + } +} + get_type_switch_completion :: proc( ast_context: ^AstContext, position_context: ^DocumentPositionContext, @@ -1987,19 +2007,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 -- cgit v1.2.3 From 7d5b8ede0f4248773d71fbfffcf9cde40c9cc774 Mon Sep 17 00:00:00 2001 From: Nathaniel Saxe Date: Sat, 7 Feb 2026 15:14:36 -0500 Subject: refactor switch case info into struct; handle multiple comma-separated variants in case block --- src/server/action_populate_switch_cases.odin | 129 +++++++++++++++------------ 1 file changed, 74 insertions(+), 55 deletions(-) (limited to 'src') diff --git a/src/server/action_populate_switch_cases.odin b/src/server/action_populate_switch_cases.odin index a733aa5..f038e7c 100644 --- a/src/server/action_populate_switch_cases.odin +++ b/src/server/action_populate_switch_cases.odin @@ -27,27 +27,32 @@ get_block_original_text :: proc(block: []^ast.Stmt, document_text: string) -> st } SwitchCaseInfo :: struct { - name: string, + names: []string, body_indentation: string, body: string, } +SwitchBlockInfo :: struct { + existing_cases: []SwitchCaseInfo, + all_covered_case_names: []string, + all_case_names: []string, + switch_indentation: string, + is_enum: bool, +} get_switch_cases_info :: proc( document: ^Document, ast_context: ^AstContext, position_context: ^DocumentPositionContext, ) -> ( - existing_cases: []SwitchCaseInfo, - all_case_names: []string, - switch_indentation: string, - is_enum: bool, + info: SwitchBlockInfo, ok: bool, ) { if (position_context.switch_stmt == nil && position_context.switch_type_stmt == nil) || (position_context.switch_stmt != nil && position_context.switch_stmt.cond == nil) { - return nil, nil, "", false, false + return {}, false } switch_block: ^ast.Block_Stmt found_switch_block: bool + is_enum: bool if position_context.switch_stmt != nil { switch_block, found_switch_block = position_context.switch_stmt.body.derived.(^ast.Block_Stmt) is_enum = true @@ -56,36 +61,40 @@ get_switch_cases_info :: proc( switch_block, found_switch_block = position_context.switch_type_stmt.body.derived.(^ast.Block_Stmt) } if !found_switch_block { - return nil, nil, "", false, false + return {}, false } - switch_indentation = get_line_indentation(string(document.text), switch_block.pos.offset) + switch_indentation := get_line_indentation(string(document.text), switch_block.pos.offset) existing_cases_in_order := make([dynamic]SwitchCaseInfo, context.temp_allocator) + all_covered_names := make([dynamic]string, context.temp_allocator) for stmt in switch_block.stmts { if case_clause, ok := stmt.derived.(^ast.Case_Clause); ok { - case_name := "" + case_names := make([dynamic]string, context.temp_allocator) for clause in case_clause.list { if is_enum { - if name, ok := get_used_switch_name(clause); ok { - case_name = name - break + if name, ok := get_used_switch_name(clause); ok && name != "" { + append(&case_names, name) + append(&all_covered_names, name) } } else { reset_ast_context(ast_context) if symbol, ok := resolve_type_expression(ast_context, clause); ok { - case_name = get_qualified_union_case_name(&symbol, ast_context, position_context) + 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) + if name == "" { + name = get_signature(ast_context, symbol) + } + if name != "" { + append(&case_names, name) + append(&all_covered_names, name) } - break } } } - if case_name != "" { + if len(case_names) > 0 { case_info := SwitchCaseInfo { - name = case_name, - body = get_block_original_text(case_clause.body, string(document.text)), + names = case_names[:], + body = get_block_original_text(case_clause.body, string(document.text)), } append(&existing_cases_in_order, case_info) } @@ -94,20 +103,27 @@ get_switch_cases_info :: proc( if is_enum { enum_value, was_super_enum, unwrap_ok := unwrap_enum(ast_context, position_context.switch_stmt.cond) if !unwrap_ok { - return nil, nil, "", true, false + return {}, false } - return existing_cases_in_order[:], enum_value.names, switch_indentation, !was_super_enum, true + return SwitchBlockInfo { + existing_cases = existing_cases_in_order[:], + all_covered_case_names = all_covered_names[:], + all_case_names = enum_value.names, + switch_indentation = switch_indentation, + is_enum = !was_super_enum, + }, + true } else { st := position_context.switch_type_stmt if st == nil { - return nil, nil, "", false, false + 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 nil, nil, "", false, false + return {}, false } - case_names := make([]string, len(union_value.types), context.temp_allocator) + 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 { @@ -117,21 +133,25 @@ get_switch_cases_info :: proc( if case_name == "" { case_name = get_signature(ast_context, symbol) } - case_names[i] = case_name + all_case_names[i] = case_name } else { - case_names[i] = "invalid type expression" + all_case_names[i] = "invalid type expression" } } - return existing_cases_in_order[:], case_names, switch_indentation, false, true + return SwitchBlockInfo { + existing_cases = existing_cases_in_order[:], + all_covered_case_names = all_covered_names[:], + all_case_names = all_case_names, + switch_indentation = switch_indentation, + is_enum = false, + }, + true } } create_populate_switch_cases_edit :: proc( position_context: ^DocumentPositionContext, - existing_cases: []SwitchCaseInfo, - switch_indentation: string, - all_case_names: []string, - is_enum: bool, + info: SwitchBlockInfo, ) -> ( TextEdit, bool, @@ -142,29 +162,38 @@ create_populate_switch_cases_edit :: proc( } //entirety of the switch block range: common.Range - if is_enum { + if info.is_enum { range = common.get_token_range(position_context.switch_stmt.body.stmt_base, position_context.file.src) } else { range = common.get_token_range(position_context.switch_type_stmt.body.stmt_base, position_context.file.src) } replacement_builder := strings.builder_make() - dot := is_enum ? "." : "" + dot := info.is_enum ? "." : "" b := &replacement_builder fmt.sbprintln(b, "{") - existing_case_names := map[string]struct{}{} - for case_info in existing_cases { - existing_case_names[case_info.name] = {} - fmt.sbprintln(b, switch_indentation, "case ", dot, case_info.name, ":", sep = "") + for case_info in info.existing_cases { + fmt.sbprint(b, info.switch_indentation, "case ", sep = "") + for name, i in case_info.names { + fmt.sbprint(b, dot, name, sep = "") + if i != len(case_info.names) - 1 { + fmt.sbprint(b, ", ", sep = "") + } + } + fmt.sbprintln(b, ":", sep = "") case_body := case_info.body if case_body != "" { fmt.sbprintln(b, case_info.body) } } - for name in all_case_names { + existing_case_names := map[string]struct{}{} + for name in info.all_covered_case_names { + existing_case_names[name] = {} + } + for name in info.all_case_names { if name in existing_case_names {continue} //covered by prev loop - fmt.sbprintln(b, switch_indentation, "case ", dot, name, ":", sep = "") + fmt.sbprintln(b, info.switch_indentation, "case ", dot, name, ":", sep = "") } - fmt.sbprint(b, switch_indentation, "}", sep = "") + fmt.sbprint(b, info.switch_indentation, "}", sep = "") return TextEdit{range = range, newText = strings.to_string(replacement_builder)}, true } @(private = "package") @@ -175,32 +204,22 @@ add_populate_switch_cases_action :: proc( uri: string, actions: ^[dynamic]CodeAction, ) { - existing_cases, all_case_names, switch_indentation, is_enum, ok := get_switch_cases_info( - document, - ast_context, - position_context, - ) + info, ok := get_switch_cases_info(document, ast_context, position_context) if !ok {return} all_cases_covered := true { existing_case_names := map[string]struct{}{} - for case_info in existing_cases { - existing_case_names[case_info.name] = {} + for name in info.all_covered_case_names { + existing_case_names[name] = {} } - for name in all_case_names { + for name in info.all_case_names { if name not_in existing_case_names { all_cases_covered = false } } } if all_cases_covered {return} //action not needed - edit, edit_ok := create_populate_switch_cases_edit( - position_context, - existing_cases, - switch_indentation, - all_case_names, - is_enum, - ) + 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) -- cgit v1.2.3