Added workspace folder support, read configs from there, use known-folders

This commit is contained in:
Alexandros Naskos 2020-05-19 22:09:00 +03:00
parent 80e1a37282
commit 7dcbc39d59
6 changed files with 202 additions and 74 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "src/known-folders"]
path = src/known-folders
url = https://github.com/ziglibs/known-folders

View File

@ -1,7 +1,7 @@
// Configuration options for zls.
/// Whether to enable snippet completions
enable_snippets: bool = true,
enable_snippets: bool = false,
/// zig library path
zig_lib_path: ?[]const u8 = null,

View File

@ -22,29 +22,11 @@ pub const Handle = struct {
allocator: *std.mem.Allocator,
handles: std.StringHashMap(*Handle),
std_uri: ?[]const u8,
pub fn init(self: *DocumentStore, allocator: *std.mem.Allocator, zig_lib_path: ?[]const u8) !void {
pub fn init(self: *DocumentStore, allocator: *std.mem.Allocator) !void {
self.allocator = allocator;
self.handles = std.StringHashMap(*Handle).init(allocator);
errdefer self.handles.deinit();
if (zig_lib_path) |zpath| {
const std_path = std.fs.path.resolve(allocator, &[_][]const u8{
zpath, "./std/std.zig",
}) catch |err| block: {
std.debug.warn("Failed to resolve zig std library path, error: {}\n", .{err});
self.std_uri = null;
return;
};
defer allocator.free(std_path);
// Get the std_path as a URI, so we can just append to it!
self.std_uri = try URI.fromPath(allocator, std_path);
std.debug.warn("Standard library base uri: {}\n", .{self.std_uri});
} else {
self.std_uri = null;
}
}
/// This function asserts the document is not open yet and takes ownership
@ -120,7 +102,7 @@ pub fn getHandle(self: *DocumentStore, uri: []const u8) ?*Handle {
}
// Check if the document text is now sane, move it to sane_text if so.
fn removeOldImports(self: *DocumentStore, handle: *Handle) !void {
fn removeOldImports(self: *DocumentStore, handle: *Handle, zig_lib_path: ?[]const u8) !void {
std.debug.warn("New text for document {}\n", .{handle.uri()});
// TODO: Better algorithm or data structure?
// Removing the imports is costly since they live in an array list
@ -144,7 +126,8 @@ fn removeOldImports(self: *DocumentStore, handle: *Handle) !void {
}
for (import_strs.items) |str| {
const uri = (try uriFromImportStr(self, &arena.allocator, handle.*, str)) orelse continue;
const std_uri = try stdUriFromLibPath(&arena.allocator, zig_lib_path);
const uri = (try uriFromImportStr(self, &arena.allocator, handle.*, str, std_uri)) orelse continue;
var idx: usize = 0;
exists_loop: while (idx < still_exist.len) : (idx += 1) {
@ -172,7 +155,12 @@ fn removeOldImports(self: *DocumentStore, handle: *Handle) !void {
}
}
pub fn applyChanges(self: *DocumentStore, handle: *Handle, content_changes: std.json.Array) !void {
pub fn applyChanges(
self: *DocumentStore,
handle: *Handle,
content_changes: std.json.Array,
zig_lib_path: ?[]const u8,
) !void {
const document = &handle.document;
for (content_changes.items) |change| {
@ -225,12 +213,18 @@ pub fn applyChanges(self: *DocumentStore, handle: *Handle, content_changes: std.
}
}
try self.removeOldImports(handle);
try self.removeOldImports(handle, zig_lib_path);
}
fn uriFromImportStr(store: *DocumentStore, allocator: *std.mem.Allocator, handle: Handle, import_str: []const u8) !?[]const u8 {
fn uriFromImportStr(
store: *DocumentStore,
allocator: *std.mem.Allocator,
handle: Handle,
import_str: []const u8,
std_uri: ?[]const u8,
) !?[]const u8 {
return if (std.mem.eql(u8, import_str, "std"))
if (store.std_uri) |std_root_uri| try std.mem.dupe(allocator, u8, std_root_uri) else {
if (std_uri) |uri| try std.mem.dupe(allocator, u8, uri) else {
std.debug.warn("Cannot resolve std library import, path is null.\n", .{});
return null;
}
@ -259,6 +253,7 @@ pub const AnalysisContext = struct {
tree: *std.zig.ast.Tree,
scope_nodes: []*std.zig.ast.Node,
last_this_node: *std.zig.ast.Node,
std_uri: ?[]const u8,
fn refreshScopeNodes(self: *AnalysisContext) !void {
var scope_nodes = std.ArrayList(*std.zig.ast.Node).init(&self.arena.allocator);
@ -269,7 +264,13 @@ pub const AnalysisContext = struct {
pub fn onImport(self: *AnalysisContext, import_str: []const u8) !?*std.zig.ast.Node {
const allocator = self.store.allocator;
const final_uri = (try uriFromImportStr(self.store, self.store.allocator, self.handle.*, import_str)) orelse return null;
const final_uri = (try uriFromImportStr(
self.store,
self.store.allocator,
self.handle.*,
import_str,
self.std_uri,
)) orelse return null;
std.debug.warn("Import final URI: {}\n", .{final_uri});
var consumed_final_uri = false;
@ -351,6 +352,7 @@ pub const AnalysisContext = struct {
.tree = tree,
.scope_nodes = self.scope_nodes,
.last_this_node = &tree.root_node.base,
.std_uri = self.std_uri,
};
}
@ -369,12 +371,36 @@ pub const AnalysisContext = struct {
}
};
pub fn analysisContext(self: *DocumentStore, handle: *Handle, arena: *std.heap.ArenaAllocator, position: types.Position) !AnalysisContext {
fn stdUriFromLibPath(allocator: *std.mem.Allocator, zig_lib_path: ?[]const u8) !?[]const u8 {
if (zig_lib_path) |zpath| {
const std_path = std.fs.path.resolve(allocator, &[_][]const u8{
zpath, "./std/std.zig",
}) catch |err| block: {
std.debug.warn("Failed to resolve zig std library path, error: {}\n", .{err});
return null;
};
defer allocator.free(std_path);
// Get the std_path as a URI, so we can just append to it!
return try URI.fromPath(allocator, std_path);
}
return null;
}
pub fn analysisContext(
self: *DocumentStore,
handle: *Handle,
arena: *std.heap.ArenaAllocator,
position: types.Position,
zig_lib_path: ?[]const u8,
) !AnalysisContext {
const tree = try handle.tree(self.allocator);
var scope_nodes = std.ArrayList(*std.zig.ast.Node).init(&arena.allocator);
try analysis.declsFromIndex(&scope_nodes, tree, try handle.document.positionToIndex(position));
const std_uri = try stdUriFromLibPath(&arena.allocator, zig_lib_path);
return AnalysisContext{
.store = self,
.handle = handle,
@ -382,6 +408,7 @@ pub fn analysisContext(self: *DocumentStore, handle: *Handle, arena: *std.heap.A
.tree = tree,
.scope_nodes = scope_nodes.items,
.last_this_node = &tree.root_node.base,
.std_uri = std_uri,
};
}
@ -400,7 +427,4 @@ pub fn deinit(self: *DocumentStore) void {
}
self.handles.deinit();
if (self.std_uri) |uri| {
self.allocator.free(uri);
}
}

1
src/known-folders Submodule

@ -0,0 +1 @@
Subproject commit 42a32b0241a5aaeaa09d7edeceefc5384b4eb466

View File

@ -8,6 +8,7 @@ const readRequestHeader = @import("header.zig").readRequestHeader;
const data = @import("data/" ++ build_options.data_version ++ ".zig");
const types = @import("types.zig");
const analysis = @import("analysis.zig");
const URI = @import("uri.zig");
// Code is largely based off of https://github.com/andersfr/zig-lsp/blob/master/server.zig
@ -15,9 +16,10 @@ var stdout: std.io.BufferedOutStream(4096, std.fs.File.OutStream) = undefined;
var allocator: *std.mem.Allocator = undefined;
var document_store: DocumentStore = undefined;
var workspace_folder_configs: std.StringHashMap(?Config) = undefined;
const initialize_response =
\\,"result":{"capabilities":{"signatureHelpProvider":{"triggerCharacters":["(",","]},"textDocumentSync":1,"completionProvider":{"resolveProvider":false,"triggerCharacters":[".",":","@"]},"documentHighlightProvider":false,"codeActionProvider":false,"declarationProvider":true,"definitionProvider":true,"typeDefinitionProvider":true,"workspace":{"workspaceFolders":{"supported":true}}}}}
\\,"result":{"capabilities":{"signatureHelpProvider":{"triggerCharacters":["(",","]},"textDocumentSync":1,"completionProvider":{"resolveProvider":false,"triggerCharacters":[".",":","@"]},"documentHighlightProvider":false,"codeActionProvider":false,"declarationProvider":true,"definitionProvider":true,"typeDefinitionProvider":true,"workspace":{"workspaceFolders":{"supported":true,"changeNotifications":true}}}}}
;
const not_implemented_response =
@ -319,11 +321,17 @@ fn gotoDefinitionGlobal(id: i64, pos_index: usize, handle: DocumentStore.Handle)
});
}
fn gotoDefinitionFieldAccess(id: i64, handle: *DocumentStore.Handle, position: types.Position, line_start_idx: usize) !void {
fn gotoDefinitionFieldAccess(
id: i64,
handle: *DocumentStore.Handle,
position: types.Position,
line_start_idx: usize,
config: Config,
) !void {
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
var analysis_ctx = try document_store.analysisContext(handle, &arena, position);
var analysis_ctx = try document_store.analysisContext(handle, &arena, position, config.zig_lib_path);
defer analysis_ctx.deinit();
const pos_index = try handle.document.positionToIndex(position);
@ -365,7 +373,7 @@ fn completeGlobal(id: i64, pos_index: usize, handle: *DocumentStore.Handle, conf
var analysis_ctx = try document_store.analysisContext(handle, &arena, types.Position{
.line = 0,
.character = 0,
});
}, config.zig_lib_path);
defer analysis_ctx.deinit();
var decl_nodes = std.ArrayList(*std.zig.ast.Node).init(&arena.allocator);
@ -390,7 +398,7 @@ fn completeFieldAccess(id: i64, handle: *DocumentStore.Handle, position: types.P
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
var analysis_ctx = try document_store.analysisContext(handle, &arena, position);
var analysis_ctx = try document_store.analysisContext(handle, &arena, position, config.zig_lib_path);
defer analysis_ctx.deinit();
var completions = std.ArrayList(types.CompletionItem).init(&arena.allocator);
@ -573,27 +581,121 @@ fn documentPositionContext(doc: types.TextDocument, pos_index: usize) PositionCo
return context;
}
fn loadConfig(folder_path: []const u8) ?Config {
var folder = std.fs.cwd().openDir(folder_path, .{}) catch return null;
defer folder.close();
const conf_file = folder.openFile("zls.json", .{}) catch return null;
defer conf_file.close();
// Max 1MB
const file_buf = conf_file.inStream().readAllAlloc(allocator, 0x1000000) catch return null;
defer allocator.free(file_buf);
// TODO: Better errors? Doesn't seem like std.json can provide us positions or context.
var config = std.json.parse(Config, &std.json.TokenStream.init(file_buf), std.json.ParseOptions{ .allocator = allocator }) catch |err| {
std.debug.warn("Error while parsing configuration file: {}\nUsing default config.\n", .{err});
return null;
};
if (config.zig_lib_path) |zig_lib_path| {
if (!std.fs.path.isAbsolute(zig_lib_path)) {
std.debug.warn("zig library path is not absolute, defaulting to null.\n", .{});
allocator.free(zig_lib_path);
config.zig_lib_path = null;
}
}
return config;
}
fn loadWorkspaceConfigs() !void {
var folder_config_it = workspace_folder_configs.iterator();
while (folder_config_it.next()) |entry| {
if (entry.value) |_| continue;
const folder_path = try URI.parse(allocator, entry.key);
defer allocator.free(folder_path);
entry.value = loadConfig(folder_path);
}
}
fn configFromUriOr(uri: []const u8, default: Config) Config {
var folder_config_it = workspace_folder_configs.iterator();
while (folder_config_it.next()) |entry| {
if (std.mem.startsWith(u8, uri, entry.key)) {
return entry.value orelse default;
}
}
return default;
}
fn processJsonRpc(parser: *std.json.Parser, json: []const u8, config: Config) !void {
var tree = try parser.parse(json);
defer tree.deinit();
const root = tree.root;
std.debug.assert(root.Object.getValue("method") != null);
const method = root.Object.getValue("method").?.String;
const id = if (root.Object.getValue("id")) |id| id.Integer else 0;
if (id == 1337 and (root.Object.getValue("method") == null or std.mem.eql(u8, root.Object.getValue("method").?.String, ""))) {
const result = (root.Object.getValue("result") orelse return).Array;
for (result.items) |workspace_folder| {
const duped_uri = try std.mem.dupe(allocator, u8, workspace_folder.Object.getValue("uri").?.String);
try workspace_folder_configs.putNoClobber(duped_uri, null);
}
try loadWorkspaceConfigs();
return;
}
std.debug.assert(root.Object.getValue("method") != null);
const method = root.Object.getValue("method").?.String;
const params = root.Object.getValue("params").?.Object;
// Core
if (std.mem.eql(u8, method, "initialize")) {
try respondGeneric(id, initialize_response);
} else if (std.mem.eql(u8, method, "initialized")) {
// noop
// Send the workspaceFolders request
try send(types.Request{
.id = .{ .Integer = 1337 },
.method = "workspace/workspaceFolders",
.params = {},
});
} else if (std.mem.eql(u8, method, "$/cancelRequest")) {
// noop
}
// Workspace folder changes
else if (std.mem.eql(u8, method, "workspace/didChangeWorkspaceFolders")) {
const event = params.getValue("event").?.Object;
const added = event.getValue("added").?.Array;
const removed = event.getValue("removed").?.Array;
for (removed.items) |rem| {
const uri = rem.Object.getValue("uri").?.String;
if (workspace_folder_configs.remove(uri)) |entry| {
allocator.free(entry.key);
if (entry.value) |c| {
std.json.parseFree(Config, c, std.json.ParseOptions{ .allocator = allocator });
}
}
}
for (added.items) |add| {
const duped_uri = try std.mem.dupe(allocator, u8, add.Object.getValue("uri").?.String);
if (try workspace_folder_configs.put(duped_uri, null)) |old| {
allocator.free(old.key);
if (old.value) |c| {
std.json.parseFree(Config, c, std.json.ParseOptions{ .allocator = allocator });
}
}
}
try loadWorkspaceConfigs();
}
// File changes
else if (std.mem.eql(u8, method, "textDocument/didOpen")) {
const document = params.getValue("textDocument").?.Object;
@ -601,7 +703,7 @@ fn processJsonRpc(parser: *std.json.Parser, json: []const u8, config: Config) !v
const text = document.getValue("text").?.String;
const handle = try document_store.openDocument(uri, text);
try publishDiagnostics(handle.*, config);
try publishDiagnostics(handle.*, configFromUriOr(uri, config));
} else if (std.mem.eql(u8, method, "textDocument/didChange")) {
const text_document = params.getValue("textDocument").?.Object;
const uri = text_document.getValue("uri").?.String;
@ -612,8 +714,9 @@ fn processJsonRpc(parser: *std.json.Parser, json: []const u8, config: Config) !v
return;
};
try document_store.applyChanges(handle, content_changes);
try publishDiagnostics(handle.*, config);
const local_config = configFromUriOr(uri, config);
try document_store.applyChanges(handle, content_changes, local_config.zig_lib_path);
try publishDiagnostics(handle.*, local_config);
} else if (std.mem.eql(u8, method, "textDocument/didSave")) {
// noop
} else if (std.mem.eql(u8, method, "textDocument/didClose")) {
@ -641,18 +744,19 @@ fn processJsonRpc(parser: *std.json.Parser, json: []const u8, config: Config) !v
const pos_index = try handle.document.positionToIndex(pos);
const pos_context = documentPositionContext(handle.document, pos_index);
const this_config = configFromUriOr(uri, config);
switch (pos_context) {
.builtin => try send(types.Response{
.id = .{ .Integer = id },
.result = .{
.CompletionList = .{
.isIncomplete = false,
.items = builtin_completions[@boolToInt(config.enable_snippets)][0..],
.items = builtin_completions[@boolToInt(this_config.enable_snippets)][0..],
},
},
}),
.var_access, .empty => try completeGlobal(id, pos_index, handle, config),
.field_access => |start_idx| try completeFieldAccess(id, handle, pos, start_idx, config),
.var_access, .empty => try completeGlobal(id, pos_index, handle, this_config),
.field_access => |start_idx| try completeFieldAccess(id, handle, pos, start_idx, this_config),
else => try respondGeneric(id, no_completions_response),
}
} else {
@ -685,7 +789,13 @@ fn processJsonRpc(parser: *std.json.Parser, json: []const u8, config: Config) !v
switch (pos_context) {
.var_access => try gotoDefinitionGlobal(id, pos_index, handle.*),
.field_access => |start_idx| try gotoDefinitionFieldAccess(id, handle, pos, start_idx),
.field_access => |start_idx| try gotoDefinitionFieldAccess(
id,
handle,
pos,
start_idx,
configFromUriOr(uri, config),
),
else => try respondGeneric(id, null_result_response),
}
}
@ -723,39 +833,31 @@ pub fn main() anyerror!void {
var config = Config{};
defer std.json.parseFree(Config, config, config_parse_options);
// TODO: Investigate using std.fs.Watch to detect writes to the config and reload it.
config_read: {
const known_folders = @import("known-folders/known-folders.zig");
const res = try known_folders.getPath(allocator, .local_configuration);
if (res) |local_config_path| {
defer allocator.free(local_config_path);
if (loadConfig(local_config_path)) |conf| {
config = conf;
break :config_read;
}
}
var exec_dir_bytes: [std.fs.MAX_PATH_BYTES]u8 = undefined;
const exec_dir_path = std.fs.selfExeDirPath(&exec_dir_bytes) catch break :config_read;
var exec_dir = std.fs.cwd().openDir(exec_dir_path, .{}) catch break :config_read;
defer exec_dir.close();
const conf_file = exec_dir.openFile("zls.json", .{}) catch break :config_read;
defer conf_file.close();
// Max 1MB
const file_buf = conf_file.inStream().readAllAlloc(allocator, 0x1000000) catch break :config_read;
defer allocator.free(file_buf);
// TODO: Better errors? Doesn't seem like std.json can provide us positions or context.
config = std.json.parse(Config, &std.json.TokenStream.init(file_buf), config_parse_options) catch |err| {
std.debug.warn("Error while parsing configuration file: {}\nUsing default config.\n", .{err});
break :config_read;
};
}
if (config.zig_lib_path) |zig_lib_path| {
if (!std.fs.path.isAbsolute(zig_lib_path)) {
std.debug.warn("zig library path is not absolute, defaulting to null.\n", .{});
allocator.free(zig_lib_path);
config.zig_lib_path = null;
if (loadConfig(exec_dir_path)) |conf| {
config = conf;
}
}
try document_store.init(allocator, config.zig_lib_path);
try document_store.init(allocator);
defer document_store.deinit();
workspace_folder_configs = std.StringHashMap(?Config).init(allocator);
defer workspace_folder_configs.deinit();
// This JSON parser is passed to processJsonRpc and reset.
var json_parser = std.json.Parser.init(allocator, false);
defer json_parser.deinit();

View File

@ -40,9 +40,7 @@ pub const RequestId = union(enum) {
};
/// Params of a request
pub const RequestParams = union(enum) {
};
pub const RequestParams = void;
pub const NotificationParams = union(enum) {
LogMessageParams: LogMessageParams,