zls/src/features/document_symbol.zig

240 lines
8.4 KiB
Zig

const std = @import("std");
const Ast = std.zig.Ast;
const log = std.log.scoped(.zls_document_symbol);
const types = @import("../lsp.zig");
const offsets = @import("../offsets.zig");
const ast = @import("../ast.zig");
const analysis = @import("../analysis.zig");
const tracy = @import("../tracy.zig");
const Symbol = struct {
name: []const u8,
detail: ?[]const u8 = null,
kind: types.SymbolKind,
loc: offsets.Loc,
selection_loc: offsets.Loc,
children: std.ArrayListUnmanaged(Symbol),
};
pub const Context = struct {
arena: std.mem.Allocator,
last_var_decl_name: ?[]const u8,
parent_node: Ast.Node.Index,
parent_symbols: *std.ArrayListUnmanaged(Symbol),
total_symbol_count: *usize,
};
fn callback(ctx: *Context, tree: Ast, node: Ast.Node.Index) error{OutOfMemory}!void {
if (node == 0) return;
const node_tags = tree.nodes.items(.tag);
const main_tokens = tree.nodes.items(.main_token);
const token_tags = tree.tokens.items(.tag);
const decl_name_token = analysis.getDeclNameToken(tree, node);
const decl_name = analysis.getDeclName(tree, node);
var new_ctx = ctx.*;
const maybe_symbol: ?Symbol = switch (node_tags[node]) {
.global_var_decl,
.local_var_decl,
.simple_var_decl,
.aligned_var_decl,
=> blk: {
new_ctx.last_var_decl_name = decl_name;
if (!ast.isContainer(tree, ctx.parent_node)) break :blk null;
const kind: types.SymbolKind = switch (token_tags[main_tokens[node]]) {
.keyword_var => .Variable,
.keyword_const => .Constant,
else => unreachable,
};
break :blk .{
.name = decl_name.?,
.detail = null,
.kind = kind,
.loc = offsets.nodeToLoc(tree, node),
.selection_loc = offsets.tokenToLoc(tree, decl_name_token.?),
.children = .{},
};
},
.test_decl,
.fn_decl,
=> |tag| blk: {
const kind: types.SymbolKind = switch (tag) {
.test_decl => .Method, // there is no SymbolKind that represents a tests
.fn_decl => .Function,
else => unreachable,
};
var buffer: [1]Ast.Node.Index = undefined;
const detail = if (tree.fullFnProto(&buffer, node)) |fn_info| analysis.getFunctionSignature(tree, fn_info) else null;
break :blk .{
.name = decl_name orelse break :blk null,
.detail = detail,
.kind = kind,
.loc = offsets.nodeToLoc(tree, node),
.selection_loc = offsets.tokenToLoc(tree, decl_name_token.?),
.children = .{},
};
},
.container_field_init,
.container_field_align,
.container_field,
=> blk: {
const kind: types.SymbolKind = switch (node_tags[ctx.parent_node]) {
.root => .Field,
.container_decl,
.container_decl_trailing,
.container_decl_arg,
.container_decl_arg_trailing,
.container_decl_two,
.container_decl_two_trailing,
=> switch (token_tags[main_tokens[ctx.parent_node]]) {
.keyword_struct => .Field,
.keyword_union => .Field,
.keyword_enum => .EnumMember,
.keyword_opaque => break :blk null,
else => unreachable,
},
.tagged_union,
.tagged_union_trailing,
.tagged_union_enum_tag,
.tagged_union_enum_tag_trailing,
.tagged_union_two,
.tagged_union_two_trailing,
=> .Field,
else => unreachable,
};
break :blk .{
.name = decl_name.?,
.detail = ctx.last_var_decl_name,
.kind = kind,
.loc = offsets.nodeToLoc(tree, node),
.selection_loc = offsets.tokenToLoc(tree, decl_name_token.?),
.children = .{},
};
},
else => null,
};
new_ctx.parent_node = node;
if (maybe_symbol) |symbol| {
var symbol_ptr = try ctx.parent_symbols.addOne(ctx.arena);
symbol_ptr.* = symbol;
new_ctx.parent_symbols = &symbol_ptr.children;
ctx.total_symbol_count.* += 1;
}
try ast.iterateChildren(tree, node, &new_ctx, error{OutOfMemory}, callback);
}
/// a mapping from a source index to a line character pair
const IndexToPositionEntry = struct {
output: *types.Position,
source_index: usize,
const Self = @This();
fn lessThan(_: void, lhs: Self, rhs: Self) bool {
return lhs.source_index < rhs.source_index;
}
};
/// converts `Symbol` to `types.DocumentSymbol`
fn convertSymbols(
arena: std.mem.Allocator,
tree: Ast,
from: []const Symbol,
total_symbol_count: usize,
encoding: offsets.Encoding,
) error{OutOfMemory}![]types.DocumentSymbol {
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
var symbol_buffer = std.ArrayListUnmanaged(types.DocumentSymbol){};
try symbol_buffer.ensureTotalCapacityPrecise(arena, total_symbol_count);
// instead of converting every `offsets.Loc` to `types.Range` by calling `offsets.locToRange`
// we instead store a mapping from source indices to their desired position, sort them by their source index
// and then iterate through them which avoids having to re-iterate through the source file to find out the line number
// this reduces algorithmic complexity from `O(file_size*symbol_count)` to `O(symbol_count*log(symbol_count))`
var mappings = std.ArrayListUnmanaged(IndexToPositionEntry){};
try mappings.ensureTotalCapacityPrecise(arena, total_symbol_count * 4);
const result = convertSymbolsInternal(from, &symbol_buffer, &mappings);
// sort items based on their source position
std.mem.sort(IndexToPositionEntry, mappings.items, {}, IndexToPositionEntry.lessThan);
var last_index: usize = 0;
var last_position: types.Position = .{ .line = 0, .character = 0 };
for (mappings.items) |mapping| {
const index = mapping.source_index;
const position = offsets.advancePosition(tree.source, last_position, last_index, index, encoding);
defer last_index = index;
defer last_position = position;
mapping.output.* = position;
}
return result;
}
fn convertSymbolsInternal(
from: []const Symbol,
symbol_buffer: *std.ArrayListUnmanaged(types.DocumentSymbol),
mappings: *std.ArrayListUnmanaged(IndexToPositionEntry),
) []types.DocumentSymbol {
// acquire storage for exactly `from.len` symbols
const prev_len = symbol_buffer.items.len;
symbol_buffer.items.len += from.len;
const to: []types.DocumentSymbol = symbol_buffer.items[prev_len..];
for (from, to) |symbol, *out| {
out.* = .{
.name = symbol.name,
.detail = symbol.detail,
.kind = symbol.kind,
// will be set later through the mapping below
.range = undefined,
.selectionRange = undefined,
.children = convertSymbolsInternal(symbol.children.items, symbol_buffer, mappings),
};
mappings.appendSliceAssumeCapacity(&[4]IndexToPositionEntry{
.{ .output = &out.range.start, .source_index = symbol.loc.start },
.{ .output = &out.selectionRange.start, .source_index = symbol.selection_loc.start },
.{ .output = &out.selectionRange.end, .source_index = symbol.selection_loc.end },
.{ .output = &out.range.end, .source_index = symbol.loc.end },
});
}
return to;
}
pub fn getDocumentSymbols(
arena: std.mem.Allocator,
tree: Ast,
encoding: offsets.Encoding,
) error{OutOfMemory}![]types.DocumentSymbol {
var root_symbols = std.ArrayListUnmanaged(Symbol){};
var total_symbol_count: usize = 0;
var ctx = Context{
.arena = arena,
.last_var_decl_name = null,
.parent_node = 0, // root-node
.parent_symbols = &root_symbols,
.total_symbol_count = &total_symbol_count,
};
try ast.iterateChildren(tree, 0, &ctx, error{OutOfMemory}, callback);
return try convertSymbols(arena, tree, root_symbols.items, ctx.total_symbol_count.*, encoding);
}