aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLouis Dutton <louis@dutton.digital>2026-01-05 11:31:27 +0000
committerLouis Dutton <louis@dutton.digital>2026-01-17 21:20:38 +0000
commit3494339e5b56ca90356dac095918d2bf02832baa (patch)
tree1c19de0a88f56b8dfc7e58aa38c1208aea12d16b /src
parent93cd45fd5077ae70158102fe7c3a1ead62e262cc (diff)
Improve hoverdoc formatting
Diffstat (limited to 'src')
-rw-r--r--src/server/analysis.odin8
-rw-r--r--src/server/ast.odin101
-rw-r--r--src/server/collector.odin4
-rw-r--r--src/server/documentation.odin16
-rw-r--r--src/server/hover.odin6
-rw-r--r--src/server/signature.odin6
-rw-r--r--src/server/symbol.odin12
7 files changed, 93 insertions, 60 deletions
diff --git a/src/server/analysis.odin b/src/server/analysis.odin
index a4227dc..cec34e3 100644
--- a/src/server/analysis.odin
+++ b/src/server/analysis.odin
@@ -1981,8 +1981,8 @@ resolve_local_identifier :: proc(ast_context: ^AstContext, node: ast.Ident, loca
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
}
@@ -2071,11 +2071,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
diff --git a/src/server/ast.odin b/src/server/ast.odin
index d1846f7..0547b83 100644
--- a/src/server/ast.odin
+++ b/src/server/ast.odin
@@ -7,6 +7,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" = {},
@@ -563,51 +564,79 @@ get_ast_node_string :: proc(node: ^ast.Node, src: string) -> string {
return 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/collector.odin b/src/server/collector.odin
index d2b8445..d8a4e65 100644
--- a/src/server/collector.odin
+++ b/src/server/collector.odin
@@ -705,12 +705,12 @@ 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)
+ symbol.comment = get_comment(comment, collection.allocator)
if expr.builtin || strings.contains(uri, "builtin.odin") {
symbol.pkg = "$builtin"
diff --git a/src/server/documentation.odin b/src/server/documentation.odin
index 2fe8842..25e4140 100644
--- a/src/server/documentation.odin
+++ b/src/server/documentation.odin
@@ -6,6 +6,10 @@ import "core:odin/ast"
import path "core:path/slashpath"
import "core:strings"
+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
build_documentation :: proc(ast_context: ^AstContext, symbol: ^Symbol, short_signature := true) {
@@ -20,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)
diff --git a/src/server/hover.odin b/src/server/hover.odin
index ca791ef..9b42f7e 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"
}
diff --git a/src/server/signature.odin b/src/server/signature.odin
index 3148951..f6c79fc 100644
--- a/src/server/signature.odin
+++ b/src/server/signature.odin
@@ -110,7 +110,7 @@ get_signature_information :: proc(
&signature_information,
SignatureInformation {
label = get_signature(symbol),
- documentation = construct_symbol_docs(symbol, markdown = false),
+ documentation = construct_symbol_docs(symbol),
},
)
}
@@ -176,7 +176,7 @@ add_proc_signature :: proc(
info := SignatureInformation {
label = get_signature(call),
- documentation = construct_symbol_docs(call, markdown = false),
+ documentation = construct_symbol_docs(call),
parameters = parameters,
}
append(signature_information, info)
@@ -204,7 +204,7 @@ add_proc_signature :: proc(
info := SignatureInformation {
label = get_signature(symbol),
- documentation = construct_symbol_docs(symbol, markdown = false),
+ documentation = construct_symbol_docs(symbol),
parameters = parameters,
}
diff --git a/src/server/symbol.odin b/src/server/symbol.odin
index 956eaa9..96c83ed 100644
--- a/src/server/symbol.odin
+++ b/src/server/symbol.odin
@@ -922,8 +922,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]
}
@@ -936,16 +936,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]
}