If the loading project has a git repository we now load the file list from git instead of scanning the directory tree. This gives us automatic .gitignore support.
805 lines
42 KiB
Zig
805 lines
42 KiB
Zig
const std = @import("std");
|
|
const tp = @import("thespian");
|
|
const cbor = @import("cbor");
|
|
const log = @import("log");
|
|
const tracy = @import("tracy");
|
|
const FileType = @import("syntax").FileType;
|
|
const root = @import("root");
|
|
const Buffer = @import("Buffer");
|
|
const builtin = @import("builtin");
|
|
|
|
const Project = @import("Project.zig");
|
|
|
|
pid: tp.pid_ref,
|
|
|
|
const Self = @This();
|
|
const module_name = @typeName(Self);
|
|
const request_timeout = std.time.ns_per_s * 5;
|
|
|
|
pub const FilePos = Project.FilePos;
|
|
|
|
pub const Error = ProjectError || ProjectManagerError;
|
|
|
|
pub const ProjectError = error{NoProject};
|
|
|
|
const SpawnError = (OutOfMemoryError || error{ThespianSpawnFailed});
|
|
const OutOfMemoryError = error{OutOfMemory};
|
|
const FileSystemError = error{FileSystem};
|
|
const SetCwdError = if (builtin.os.tag == .windows) error{UnrecognizedVolume} else error{};
|
|
const CallError = tp.CallError;
|
|
const ProjectManagerError = (SpawnError || error{ProjectManagerFailed});
|
|
|
|
pub fn get() SpawnError!Self {
|
|
const pid = tp.env.get().proc(module_name);
|
|
return if (pid.expired()) create() else .{ .pid = pid };
|
|
}
|
|
|
|
fn send(message: anytype) ProjectManagerError!void {
|
|
return (try get()).pid.send(message) catch error.ProjectManagerFailed;
|
|
}
|
|
|
|
fn create() SpawnError!Self {
|
|
const pid = try Process.create();
|
|
defer pid.deinit();
|
|
tp.env.get().proc_set(module_name, pid.ref());
|
|
return .{ .pid = tp.env.get().proc(module_name) };
|
|
}
|
|
|
|
pub fn start() SpawnError!void {
|
|
_ = try get();
|
|
}
|
|
|
|
pub fn shutdown() void {
|
|
const pid = tp.env.get().proc(module_name);
|
|
if (pid.expired()) {
|
|
tp.self_pid().send(.{ "project_manager", "shutdown" }) catch {};
|
|
return;
|
|
}
|
|
pid.send(.{"shutdown"}) catch {};
|
|
}
|
|
|
|
pub fn open(rel_project_directory: []const u8) (ProjectManagerError || FileSystemError || std.fs.File.OpenError || SetCwdError)!void {
|
|
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
|
|
const project_directory = std.fs.cwd().realpath(rel_project_directory, &path_buf) catch "(none)";
|
|
const current_project = tp.env.get().str("project");
|
|
if (std.mem.eql(u8, current_project, project_directory)) return;
|
|
var dir = try std.fs.openDirAbsolute(project_directory, .{});
|
|
try dir.setAsCwd();
|
|
dir.close();
|
|
tp.env.get().str_set("project", project_directory);
|
|
return send(.{ "open", project_directory });
|
|
}
|
|
|
|
pub fn close(project_directory: []const u8) (ProjectManagerError || error{CloseCurrentProject})!void {
|
|
const current_project = tp.env.get().str("project");
|
|
if (std.mem.eql(u8, current_project, project_directory)) return error.CloseCurrentProject;
|
|
return send(.{ "close", project_directory });
|
|
}
|
|
|
|
pub fn request_n_most_recent_file(allocator: std.mem.Allocator, n: usize) (CallError || ProjectError || cbor.Error)!?[]const u8 {
|
|
const project = tp.env.get().str("project");
|
|
if (project.len == 0)
|
|
return error.NoProject;
|
|
const rsp = try (try get()).pid.call(allocator, request_timeout, .{ "request_n_most_recent_file", project, n });
|
|
defer allocator.free(rsp.buf);
|
|
var file_path: []const u8 = undefined;
|
|
return if (try cbor.match(rsp.buf, .{tp.extract(&file_path)})) try allocator.dupe(u8, file_path) else null;
|
|
}
|
|
|
|
pub fn request_most_recent_file(allocator: std.mem.Allocator) (CallError || ProjectError || cbor.Error)!?[]const u8 {
|
|
return request_n_most_recent_file(allocator, 0);
|
|
}
|
|
|
|
pub fn request_recent_files(max: usize) (ProjectManagerError || ProjectError)!void {
|
|
const project = tp.env.get().str("project");
|
|
if (project.len == 0)
|
|
return error.NoProject;
|
|
return send(.{ "request_recent_files", project, max });
|
|
}
|
|
|
|
pub fn request_recent_projects(allocator: std.mem.Allocator) (ProjectError || CallError)!tp.message {
|
|
const project = tp.env.get().str("project");
|
|
return (try get()).pid.call(allocator, request_timeout, .{ "request_recent_projects", project });
|
|
}
|
|
|
|
pub fn query_recent_files(max: usize, query: []const u8) (ProjectManagerError || ProjectError)!void {
|
|
const project = tp.env.get().str("project");
|
|
if (project.len == 0)
|
|
return error.NoProject;
|
|
return send(.{ "query_recent_files", project, max, query });
|
|
}
|
|
|
|
pub fn request_path_files(max: usize, path: []const u8) (ProjectManagerError || ProjectError)!void {
|
|
const project = tp.env.get().str("project");
|
|
if (project.len == 0)
|
|
return error.NoProject;
|
|
return send(.{ "request_path_files", project, max, path });
|
|
}
|
|
|
|
pub fn request_tasks(allocator: std.mem.Allocator) (ProjectError || CallError)!tp.message {
|
|
const project = tp.env.get().str("project");
|
|
if (project.len == 0)
|
|
return error.NoProject;
|
|
return (try get()).pid.call(allocator, request_timeout, .{ "request_tasks", project });
|
|
}
|
|
|
|
pub fn add_task(task: []const u8) (ProjectManagerError || ProjectError)!void {
|
|
const project = tp.env.get().str("project");
|
|
if (project.len == 0)
|
|
return error.NoProject;
|
|
return send(.{ "add_task", project, task });
|
|
}
|
|
|
|
pub fn delete_task(task: []const u8) (ProjectManagerError || ProjectError)!void {
|
|
const project = tp.env.get().str("project");
|
|
if (project.len == 0)
|
|
return error.NoProject;
|
|
return send(.{ "delete_task", project, task });
|
|
}
|
|
|
|
pub fn did_open(file_path: []const u8, file_type: *const FileType, version: usize, text: []const u8, ephemeral: bool) (ProjectManagerError || ProjectError)!void {
|
|
if (ephemeral) return;
|
|
const project = tp.env.get().str("project");
|
|
if (project.len == 0)
|
|
return error.NoProject;
|
|
const text_ptr: usize = if (text.len > 0) @intFromPtr(text.ptr) else 0;
|
|
const language_server = file_type.language_server orelse return;
|
|
return send(.{ "did_open", project, file_path, file_type.name, language_server, version, text_ptr, text.len });
|
|
}
|
|
|
|
pub fn did_change(file_path: []const u8, version: usize, root_dst: usize, root_src: usize, eol_mode: Buffer.EolMode) (ProjectManagerError || ProjectError)!void {
|
|
const project = tp.env.get().str("project");
|
|
if (project.len == 0)
|
|
return error.NoProject;
|
|
return send(.{ "did_change", project, file_path, version, root_dst, root_src, @intFromEnum(eol_mode) });
|
|
}
|
|
|
|
pub fn did_save(file_path: []const u8) (ProjectManagerError || ProjectError)!void {
|
|
const project = tp.env.get().str("project");
|
|
if (project.len == 0)
|
|
return error.NoProject;
|
|
return send(.{ "did_save", project, file_path });
|
|
}
|
|
|
|
pub fn did_close(file_path: []const u8) (ProjectManagerError || ProjectError)!void {
|
|
const project = tp.env.get().str("project");
|
|
if (project.len == 0)
|
|
return error.NoProject;
|
|
return send(.{ "did_close", project, file_path });
|
|
}
|
|
|
|
pub fn goto_definition(file_path: []const u8, row: usize, col: usize) (ProjectManagerError || ProjectError)!void {
|
|
const project = tp.env.get().str("project");
|
|
if (project.len == 0)
|
|
return error.NoProject;
|
|
return send(.{ "goto_definition", project, file_path, row, col });
|
|
}
|
|
|
|
pub fn goto_declaration(file_path: []const u8, row: usize, col: usize) (ProjectManagerError || ProjectError)!void {
|
|
const project = tp.env.get().str("project");
|
|
if (project.len == 0)
|
|
return error.NoProject;
|
|
return send(.{ "goto_declaration", project, file_path, row, col });
|
|
}
|
|
|
|
pub fn goto_implementation(file_path: []const u8, row: usize, col: usize) (ProjectManagerError || ProjectError)!void {
|
|
const project = tp.env.get().str("project");
|
|
if (project.len == 0)
|
|
return error.NoProject;
|
|
return send(.{ "goto_implementation", project, file_path, row, col });
|
|
}
|
|
|
|
pub fn goto_type_definition(file_path: []const u8, row: usize, col: usize) (ProjectManagerError || ProjectError)!void {
|
|
const project = tp.env.get().str("project");
|
|
if (project.len == 0)
|
|
return error.NoProject;
|
|
return send(.{ "goto_type_definition", project, file_path, row, col });
|
|
}
|
|
|
|
pub fn references(file_path: []const u8, row: usize, col: usize) (ProjectManagerError || ProjectError)!void {
|
|
const project = tp.env.get().str("project");
|
|
if (project.len == 0)
|
|
return error.NoProject;
|
|
return send(.{ "references", project, file_path, row, col });
|
|
}
|
|
|
|
pub fn completion(file_path: []const u8, row: usize, col: usize) (ProjectManagerError || ProjectError)!void {
|
|
const project = tp.env.get().str("project");
|
|
if (project.len == 0)
|
|
return error.NoProject;
|
|
return send(.{ "completion", project, file_path, row, col });
|
|
}
|
|
|
|
pub fn rename_symbol(file_path: []const u8, row: usize, col: usize) (ProjectManagerError || ProjectError)!void {
|
|
const project = tp.env.get().str("project");
|
|
if (project.len == 0)
|
|
return error.NoProject;
|
|
return send(.{ "rename_symbol", project, file_path, row, col });
|
|
}
|
|
|
|
pub fn hover(file_path: []const u8, row: usize, col: usize) (ProjectManagerError || ProjectError)!void {
|
|
const project = tp.env.get().str("project");
|
|
if (project.len == 0)
|
|
return error.NoProject;
|
|
return send(.{ "hover", project, file_path, row, col });
|
|
}
|
|
|
|
pub fn update_mru(file_path: []const u8, row: usize, col: usize, ephemeral: bool) (ProjectManagerError || ProjectError)!void {
|
|
if (ephemeral) return;
|
|
const project = tp.env.get().str("project");
|
|
if (project.len == 0)
|
|
return error.NoProject;
|
|
return send(.{ "update_mru", project, file_path, row, col });
|
|
}
|
|
|
|
pub fn get_mru_position(allocator: std.mem.Allocator, file_path: []const u8, ctx: anytype) (ProjectManagerError || ProjectError)!void {
|
|
const project = tp.env.get().str("project");
|
|
if (project.len == 0)
|
|
return error.NoProject;
|
|
|
|
const cp = @import("completion.zig");
|
|
return cp.send(allocator, (try get()).pid, .{ "get_mru_position", project, file_path }, ctx);
|
|
}
|
|
|
|
const Process = struct {
|
|
allocator: std.mem.Allocator,
|
|
parent: tp.pid,
|
|
logger: log.Logger,
|
|
receiver: Receiver,
|
|
projects: ProjectsMap,
|
|
|
|
const InvalidArgumentError = error{InvalidArgument};
|
|
const UnsupportedError = error{Unsupported};
|
|
|
|
const Receiver = tp.Receiver(*Process);
|
|
const ProjectsMap = std.StringHashMapUnmanaged(*Project);
|
|
const RecentProject = struct {
|
|
name: []const u8,
|
|
last_used: i128,
|
|
};
|
|
|
|
fn create() SpawnError!tp.pid {
|
|
const allocator = std.heap.c_allocator;
|
|
const self = try allocator.create(Process);
|
|
self.* = .{
|
|
.allocator = allocator,
|
|
.parent = tp.self_pid().clone(),
|
|
.logger = log.logger(module_name),
|
|
.receiver = Receiver.init(Process.receive, self),
|
|
.projects = .empty,
|
|
};
|
|
return tp.spawn_link(self.allocator, self, Process.start, module_name);
|
|
}
|
|
|
|
fn deinit(self: *Process) void {
|
|
var i = self.projects.iterator();
|
|
while (i.next()) |p| {
|
|
self.allocator.free(p.key_ptr.*);
|
|
p.value_ptr.*.deinit();
|
|
self.allocator.destroy(p.value_ptr.*);
|
|
}
|
|
self.projects.deinit(self.allocator);
|
|
self.parent.deinit();
|
|
self.logger.deinit();
|
|
self.allocator.destroy(self);
|
|
}
|
|
|
|
fn start(self: *Process) tp.result {
|
|
_ = tp.set_trap(true);
|
|
tp.receive(&self.receiver);
|
|
}
|
|
|
|
fn receive(self: *Process, from: tp.pid_ref, m: tp.message) tp.result {
|
|
errdefer self.deinit();
|
|
return self.receive_safe(from, m) catch |e| switch (e) {
|
|
error.ExitNormal => tp.exit_normal(),
|
|
error.ClientFailed => {
|
|
const err = tp.exit_error(e, @errorReturnTrace());
|
|
self.logger.err("receive", err);
|
|
return err;
|
|
},
|
|
else => {
|
|
const err = tp.exit_error(e, @errorReturnTrace());
|
|
self.logger.err("receive", err);
|
|
},
|
|
};
|
|
}
|
|
|
|
fn receive_safe(self: *Process, from: tp.pid_ref, m: tp.message) (error{ ExitNormal, ClientFailed } || cbor.Error)!void {
|
|
var project_directory: []const u8 = undefined;
|
|
var path: []const u8 = undefined;
|
|
var query: []const u8 = undefined;
|
|
var file_type: []const u8 = undefined;
|
|
var language_server: []const u8 = undefined;
|
|
var method: []const u8 = undefined;
|
|
var cbor_id: []const u8 = undefined;
|
|
var params_cb: []const u8 = undefined;
|
|
var high: i64 = 0;
|
|
var low: i64 = 0;
|
|
var max: usize = 0;
|
|
var row: usize = 0;
|
|
var col: usize = 0;
|
|
var version: usize = 0;
|
|
var text_ptr: usize = 0;
|
|
var text_len: usize = 0;
|
|
var n: usize = 0;
|
|
var task: []const u8 = undefined;
|
|
var context: usize = undefined;
|
|
|
|
var root_dst: usize = 0;
|
|
var root_src: usize = 0;
|
|
var eol_mode: Buffer.EolModeTag = @intFromEnum(Buffer.EolMode.lf);
|
|
|
|
if (try cbor.match(m.buf, .{ "walk_tree_entry", tp.extract(&project_directory), tp.extract(&path), tp.extract(&high), tp.extract(&low) })) {
|
|
const mtime = (@as(i128, @intCast(high)) << 64) | @as(i128, @intCast(low));
|
|
if (self.projects.get(project_directory)) |project|
|
|
project.walk_tree_entry(path, mtime) catch |e| self.logger.err("walk_tree_entry", e);
|
|
} else if (try cbor.match(m.buf, .{ "walk_tree_done", tp.extract(&project_directory) })) {
|
|
if (self.projects.get(project_directory)) |project|
|
|
project.walk_tree_done() catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
|
|
} else if (try cbor.match(m.buf, .{ "git", tp.extract(&context), tp.more })) {
|
|
const project: *Project = @ptrFromInt(context);
|
|
project.process_git(m) catch {};
|
|
} else if (try cbor.match(m.buf, .{ "update_mru", tp.extract(&project_directory), tp.extract(&path), tp.extract(&row), tp.extract(&col) })) {
|
|
self.update_mru(project_directory, path, row, col) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
|
|
} else if (try cbor.match(m.buf, .{ "child", tp.extract(&project_directory), tp.extract(&language_server), "notify", tp.extract(&method), tp.extract_cbor(¶ms_cb) })) {
|
|
self.dispatch_notify(project_directory, language_server, method, params_cb) catch |e| return self.logger.err("lsp-handling", e);
|
|
} else if (try cbor.match(m.buf, .{ "child", tp.extract(&project_directory), tp.extract(&language_server), "request", tp.extract(&method), tp.extract_cbor(&cbor_id), tp.extract_cbor(¶ms_cb) })) {
|
|
self.dispatch_request(from, project_directory, language_server, method, cbor_id, params_cb) catch |e| return self.logger.err("lsp-handling", e);
|
|
} else if (try cbor.match(m.buf, .{ "child", tp.extract(&path), "not found" })) {
|
|
self.logger.print("executable '{s}' not found", .{path});
|
|
} else if (try cbor.match(m.buf, .{ "child", tp.extract(&path), "done" })) {
|
|
self.logger.print_err("lsp-handling", "child '{s}' terminated", .{path});
|
|
} else if (try cbor.match(m.buf, .{ "open", tp.extract(&project_directory) })) {
|
|
self.open(project_directory) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
|
|
} else if (try cbor.match(m.buf, .{ "close", tp.extract(&project_directory) })) {
|
|
self.close(project_directory) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
|
|
} else if (try cbor.match(m.buf, .{ "request_n_most_recent_file", tp.extract(&project_directory), tp.extract(&n) })) {
|
|
self.request_n_most_recent_file(from, project_directory, n) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
|
|
} else if (try cbor.match(m.buf, .{ "request_recent_files", tp.extract(&project_directory), tp.extract(&max) })) {
|
|
self.request_recent_files(from, project_directory, max) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
|
|
} else if (try cbor.match(m.buf, .{ "request_recent_projects", tp.extract(&project_directory) })) {
|
|
self.request_recent_projects(from, project_directory) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
|
|
} else if (try cbor.match(m.buf, .{ "query_recent_files", tp.extract(&project_directory), tp.extract(&max), tp.extract(&query) })) {
|
|
self.query_recent_files(from, project_directory, max, query) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
|
|
} else if (try cbor.match(m.buf, .{ "request_path_files", tp.extract(&project_directory), tp.extract(&max), tp.extract(&path) })) {
|
|
self.request_path_files(from, project_directory, max, path) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
|
|
} else if (try cbor.match(m.buf, .{ "request_tasks", tp.extract(&project_directory) })) {
|
|
self.request_tasks(from, project_directory) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
|
|
} else if (try cbor.match(m.buf, .{ "add_task", tp.extract(&project_directory), tp.extract(&task) })) {
|
|
self.add_task(project_directory, task) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
|
|
} else if (try cbor.match(m.buf, .{ "delete_task", tp.extract(&project_directory), tp.extract(&task) })) {
|
|
self.delete_task(project_directory, task) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
|
|
} else if (try cbor.match(m.buf, .{ "did_open", tp.extract(&project_directory), tp.extract(&path), tp.extract(&file_type), tp.extract_cbor(&language_server), tp.extract(&version), tp.extract(&text_ptr), tp.extract(&text_len) })) {
|
|
const text = if (text_len > 0) @as([*]const u8, @ptrFromInt(text_ptr))[0..text_len] else "";
|
|
self.did_open(project_directory, path, file_type, language_server, version, text) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
|
|
} else if (try cbor.match(m.buf, .{ "did_change", tp.extract(&project_directory), tp.extract(&path), tp.extract(&version), tp.extract(&root_dst), tp.extract(&root_src), tp.extract(&eol_mode) })) {
|
|
self.did_change(project_directory, path, version, root_dst, root_src, @enumFromInt(eol_mode)) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
|
|
} else if (try cbor.match(m.buf, .{ "did_save", tp.extract(&project_directory), tp.extract(&path) })) {
|
|
self.did_save(project_directory, path) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
|
|
} else if (try cbor.match(m.buf, .{ "did_close", tp.extract(&project_directory), tp.extract(&path) })) {
|
|
self.did_close(project_directory, path) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
|
|
} else if (try cbor.match(m.buf, .{ "goto_definition", tp.extract(&project_directory), tp.extract(&path), tp.extract(&row), tp.extract(&col) })) {
|
|
self.goto_definition(from, project_directory, path, row, col) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
|
|
} else if (try cbor.match(m.buf, .{ "goto_declaration", tp.extract(&project_directory), tp.extract(&path), tp.extract(&row), tp.extract(&col) })) {
|
|
self.goto_declaration(from, project_directory, path, row, col) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
|
|
} else if (try cbor.match(m.buf, .{ "goto_implementation", tp.extract(&project_directory), tp.extract(&path), tp.extract(&row), tp.extract(&col) })) {
|
|
self.goto_implementation(from, project_directory, path, row, col) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
|
|
} else if (try cbor.match(m.buf, .{ "goto_type_definition", tp.extract(&project_directory), tp.extract(&path), tp.extract(&row), tp.extract(&col) })) {
|
|
self.goto_type_definition(from, project_directory, path, row, col) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
|
|
} else if (try cbor.match(m.buf, .{ "references", tp.extract(&project_directory), tp.extract(&path), tp.extract(&row), tp.extract(&col) })) {
|
|
self.references(from, project_directory, path, row, col) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
|
|
} else if (try cbor.match(m.buf, .{ "completion", tp.extract(&project_directory), tp.extract(&path), tp.extract(&row), tp.extract(&col) })) {
|
|
self.completion(from, project_directory, path, row, col) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
|
|
} else if (try cbor.match(m.buf, .{ "rename_symbol", tp.extract(&project_directory), tp.extract(&path), tp.extract(&row), tp.extract(&col) })) {
|
|
self.rename_symbol(from, project_directory, path, row, col) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
|
|
} else if (try cbor.match(m.buf, .{ "hover", tp.extract(&project_directory), tp.extract(&path), tp.extract(&row), tp.extract(&col) })) {
|
|
self.hover(from, project_directory, path, row, col) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
|
|
} else if (try cbor.match(m.buf, .{ "get_mru_position", tp.extract(&project_directory), tp.extract(&path) })) {
|
|
self.get_mru_position(from, project_directory, path) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
|
|
} else if (try cbor.match(m.buf, .{"shutdown"})) {
|
|
self.persist_projects();
|
|
from.send(.{ "project_manager", "shutdown" }) catch return error.ClientFailed;
|
|
return error.ExitNormal;
|
|
} else if (try cbor.match(m.buf, .{ "exit", "normal" })) {
|
|
return;
|
|
} else if (try cbor.match(m.buf, .{ "exit", "DEADSEND", tp.more })) {
|
|
return;
|
|
} else if (try cbor.match(m.buf, .{ "exit", "error.FileNotFound", tp.more })) {
|
|
return;
|
|
} else {
|
|
self.logger.err("receive", tp.unexpected(m));
|
|
}
|
|
}
|
|
|
|
fn open(self: *Process, project_directory: []const u8) (SpawnError || std.fs.Dir.OpenError)!void {
|
|
if (self.projects.get(project_directory) == null) {
|
|
self.logger.print("opening: {s}", .{project_directory});
|
|
const project = try self.allocator.create(Project);
|
|
project.* = try Project.init(self.allocator, project_directory);
|
|
try self.projects.put(self.allocator, try self.allocator.dupe(u8, project_directory), project);
|
|
self.restore_project(project) catch |e| self.logger.err("restore_project", e);
|
|
project.query_git();
|
|
} else {
|
|
self.logger.print("switched to: {s}", .{project_directory});
|
|
}
|
|
}
|
|
|
|
fn close(self: *Process, project_directory: []const u8) error{}!void {
|
|
if (self.projects.fetchRemove(project_directory)) |kv| {
|
|
self.allocator.free(kv.key);
|
|
self.persist_project(kv.value) catch {};
|
|
kv.value.deinit();
|
|
self.allocator.destroy(kv.value);
|
|
self.logger.print("closed: {s}", .{project_directory});
|
|
}
|
|
}
|
|
|
|
fn request_n_most_recent_file(self: *Process, from: tp.pid_ref, project_directory: []const u8, n: usize) (ProjectError || Project.ClientError)!void {
|
|
const project = self.projects.get(project_directory) orelse return error.NoProject;
|
|
return project.request_n_most_recent_file(from, n);
|
|
}
|
|
|
|
fn request_recent_files(self: *Process, from: tp.pid_ref, project_directory: []const u8, max: usize) (ProjectError || Project.ClientError)!void {
|
|
const project = self.projects.get(project_directory) orelse return error.NoProject;
|
|
return project.request_recent_files(from, max);
|
|
}
|
|
|
|
fn request_recent_projects(self: *Process, from: tp.pid_ref, project_directory: []const u8) (ProjectError || Project.ClientError)!void {
|
|
var recent_projects = std.ArrayList(RecentProject).init(self.allocator);
|
|
defer recent_projects.deinit();
|
|
self.load_recent_projects(&recent_projects, project_directory) catch {};
|
|
self.sort_projects_by_last_used(&recent_projects);
|
|
var message = std.ArrayList(u8).init(self.allocator);
|
|
const writer = message.writer();
|
|
try cbor.writeArrayHeader(writer, recent_projects.items.len);
|
|
for (recent_projects.items) |project| {
|
|
try cbor.writeArrayHeader(writer, 2);
|
|
try cbor.writeValue(writer, project.name);
|
|
try cbor.writeValue(writer, if (self.projects.get(project.name)) |_| true else false);
|
|
}
|
|
from.send_raw(.{ .buf = message.items }) catch return error.ClientFailed;
|
|
}
|
|
|
|
fn query_recent_files(self: *Process, from: tp.pid_ref, project_directory: []const u8, max: usize, query: []const u8) (ProjectError || Project.ClientError)!void {
|
|
const project = self.projects.get(project_directory) orelse return error.NoProject;
|
|
const start_time = std.time.milliTimestamp();
|
|
const matched = try project.query_recent_files(from, max, query);
|
|
const query_time = std.time.milliTimestamp() - start_time;
|
|
if (query_time > 250)
|
|
self.logger.print("query \"{s}\" matched {d}/{d} in {d} ms", .{ query, matched, project.files.items.len, query_time });
|
|
}
|
|
|
|
fn request_path_files(self: *Process, from: tp.pid_ref, project_directory: []const u8, max: usize, path: []const u8) (ProjectError || SpawnError || std.fs.Dir.OpenError)!void {
|
|
const project = self.projects.get(project_directory) orelse return error.NoProject;
|
|
try request_path_files_async(self.allocator, from, project, max, path);
|
|
}
|
|
|
|
fn request_tasks(self: *Process, from: tp.pid_ref, project_directory: []const u8) (ProjectError || Project.ClientError)!void {
|
|
const project = self.projects.get(project_directory) orelse return error.NoProject;
|
|
try project.request_tasks(from);
|
|
}
|
|
|
|
fn add_task(self: *Process, project_directory: []const u8, task: []const u8) (ProjectError || Project.ClientError)!void {
|
|
const project = self.projects.get(project_directory) orelse return error.NoProject;
|
|
try project.add_task(task);
|
|
}
|
|
|
|
fn delete_task(self: *Process, project_directory: []const u8, task: []const u8) (ProjectError || Project.ClientError)!void {
|
|
const project = self.projects.get(project_directory) orelse return error.NoProject;
|
|
try project.delete_task(task);
|
|
}
|
|
|
|
fn did_open(self: *Process, project_directory: []const u8, file_path: []const u8, file_type: []const u8, language_server: []const u8, version: usize, text: []const u8) (ProjectError || Project.StartLspError || CallError || cbor.Error)!void {
|
|
const frame = tracy.initZone(@src(), .{ .name = module_name ++ ".did_open" });
|
|
defer frame.deinit();
|
|
const project = self.projects.get(project_directory) orelse return error.NoProject;
|
|
return project.did_open(file_path, file_type, language_server, version, text);
|
|
}
|
|
|
|
fn did_change(self: *Process, project_directory: []const u8, file_path: []const u8, version: usize, root_dst: usize, root_src: usize, eol_mode: Buffer.EolMode) (ProjectError || Project.LspError)!void {
|
|
const frame = tracy.initZone(@src(), .{ .name = module_name ++ ".did_change" });
|
|
defer frame.deinit();
|
|
const project = self.projects.get(project_directory) orelse return error.NoProject;
|
|
return project.did_change(file_path, version, root_dst, root_src, eol_mode);
|
|
}
|
|
|
|
fn did_save(self: *Process, project_directory: []const u8, file_path: []const u8) (ProjectError || Project.LspError)!void {
|
|
const frame = tracy.initZone(@src(), .{ .name = module_name ++ ".did_save" });
|
|
defer frame.deinit();
|
|
const project = self.projects.get(project_directory) orelse return error.NoProject;
|
|
return project.did_save(file_path);
|
|
}
|
|
|
|
fn did_close(self: *Process, project_directory: []const u8, file_path: []const u8) (ProjectError || Project.LspError)!void {
|
|
const frame = tracy.initZone(@src(), .{ .name = module_name ++ ".did_close" });
|
|
defer frame.deinit();
|
|
const project = self.projects.get(project_directory) orelse return error.NoProject;
|
|
return project.did_close(file_path);
|
|
}
|
|
|
|
fn goto_definition(self: *Process, from: tp.pid_ref, project_directory: []const u8, file_path: []const u8, row: usize, col: usize) (ProjectError || Project.SendGotoRequestError)!void {
|
|
const frame = tracy.initZone(@src(), .{ .name = module_name ++ ".goto_definition" });
|
|
defer frame.deinit();
|
|
const project = self.projects.get(project_directory) orelse return error.NoProject;
|
|
return project.goto_definition(from, file_path, row, col);
|
|
}
|
|
|
|
fn goto_declaration(self: *Process, from: tp.pid_ref, project_directory: []const u8, file_path: []const u8, row: usize, col: usize) (ProjectError || Project.SendGotoRequestError)!void {
|
|
const frame = tracy.initZone(@src(), .{ .name = module_name ++ ".goto_declaration" });
|
|
defer frame.deinit();
|
|
const project = self.projects.get(project_directory) orelse return error.NoProject;
|
|
return project.goto_declaration(from, file_path, row, col);
|
|
}
|
|
|
|
fn goto_implementation(self: *Process, from: tp.pid_ref, project_directory: []const u8, file_path: []const u8, row: usize, col: usize) (ProjectError || Project.SendGotoRequestError)!void {
|
|
const frame = tracy.initZone(@src(), .{ .name = module_name ++ ".goto_implementation" });
|
|
defer frame.deinit();
|
|
const project = self.projects.get(project_directory) orelse return error.NoProject;
|
|
return project.goto_implementation(from, file_path, row, col);
|
|
}
|
|
|
|
fn goto_type_definition(self: *Process, from: tp.pid_ref, project_directory: []const u8, file_path: []const u8, row: usize, col: usize) (ProjectError || Project.SendGotoRequestError)!void {
|
|
const frame = tracy.initZone(@src(), .{ .name = module_name ++ ".goto_type_definition" });
|
|
defer frame.deinit();
|
|
const project = self.projects.get(project_directory) orelse return error.NoProject;
|
|
return project.goto_type_definition(from, file_path, row, col);
|
|
}
|
|
|
|
fn references(self: *Process, from: tp.pid_ref, project_directory: []const u8, file_path: []const u8, row: usize, col: usize) (ProjectError || Project.SendGotoRequestError)!void {
|
|
const frame = tracy.initZone(@src(), .{ .name = module_name ++ ".references" });
|
|
defer frame.deinit();
|
|
const project = self.projects.get(project_directory) orelse return error.NoProject;
|
|
return project.references(from, file_path, row, col);
|
|
}
|
|
|
|
fn completion(self: *Process, from: tp.pid_ref, project_directory: []const u8, file_path: []const u8, row: usize, col: usize) (ProjectError || Project.InvalidMessageError || Project.LspOrClientError || cbor.Error)!void {
|
|
const frame = tracy.initZone(@src(), .{ .name = module_name ++ ".completion" });
|
|
defer frame.deinit();
|
|
const project = self.projects.get(project_directory) orelse return error.NoProject;
|
|
return project.completion(from, file_path, row, col);
|
|
}
|
|
|
|
fn rename_symbol(self: *Process, from: tp.pid_ref, project_directory: []const u8, file_path: []const u8, row: usize, col: usize) (ProjectError || Project.InvalidMessageError || Project.LspOrClientError || Project.GetLineOfFileError || cbor.Error)!void {
|
|
const frame = tracy.initZone(@src(), .{ .name = module_name ++ ".rename_symbol" });
|
|
defer frame.deinit();
|
|
const project = self.projects.get(project_directory) orelse return error.NoProject;
|
|
return project.rename_symbol(from, file_path, row, col);
|
|
}
|
|
|
|
fn hover(self: *Process, from: tp.pid_ref, project_directory: []const u8, file_path: []const u8, row: usize, col: usize) (ProjectError || Project.InvalidMessageError || Project.LspOrClientError || cbor.Error)!void {
|
|
const frame = tracy.initZone(@src(), .{ .name = module_name ++ ".hover" });
|
|
defer frame.deinit();
|
|
const project = self.projects.get(project_directory) orelse return error.NoProject;
|
|
return project.hover(from, file_path, row, col);
|
|
}
|
|
|
|
fn get_mru_position(self: *Process, from: tp.pid_ref, project_directory: []const u8, file_path: []const u8) (ProjectError || Project.ClientError)!void {
|
|
const frame = tracy.initZone(@src(), .{ .name = module_name ++ ".get_mru_position" });
|
|
defer frame.deinit();
|
|
const project = self.projects.get(project_directory) orelse return error.NoProject;
|
|
return project.get_mru_position(from, file_path);
|
|
}
|
|
|
|
fn update_mru(self: *Process, project_directory: []const u8, file_path: []const u8, row: usize, col: usize) (ProjectError || OutOfMemoryError)!void {
|
|
const project = self.projects.get(project_directory) orelse return error.NoProject;
|
|
return project.update_mru(file_path, row, col);
|
|
}
|
|
|
|
fn dispatch_notify(self: *Process, project_directory: []const u8, language_server: []const u8, method: []const u8, params_cb: []const u8) (ProjectError || Project.ClientError || Project.InvalidMessageError || cbor.Error || cbor.JsonEncodeError)!void {
|
|
_ = language_server;
|
|
const project = self.projects.get(project_directory) orelse return error.NoProject;
|
|
return if (std.mem.eql(u8, method, "textDocument/publishDiagnostics"))
|
|
project.publish_diagnostics(self.parent.ref(), params_cb)
|
|
else if (std.mem.eql(u8, method, "window/showMessage"))
|
|
project.show_message(self.parent.ref(), params_cb)
|
|
else if (std.mem.eql(u8, method, "window/logMessage"))
|
|
project.show_message(self.parent.ref(), params_cb)
|
|
else {
|
|
const params = try cbor.toJsonAlloc(self.allocator, params_cb);
|
|
defer self.allocator.free(params);
|
|
self.logger.print("LSP notification: {s} -> {s}", .{ method, params });
|
|
};
|
|
}
|
|
|
|
fn dispatch_request(self: *Process, from: tp.pid_ref, project_directory: []const u8, language_server: []const u8, method: []const u8, cbor_id: []const u8, params_cb: []const u8) (ProjectError || Project.ClientError || cbor.Error || cbor.JsonEncodeError || UnsupportedError)!void {
|
|
_ = language_server;
|
|
const project = if (self.projects.get(project_directory)) |p| p else return error.NoProject;
|
|
return if (std.mem.eql(u8, method, "client/registerCapability"))
|
|
project.register_capability(from, cbor_id, params_cb)
|
|
else if (std.mem.eql(u8, method, "window/workDoneProgress/create"))
|
|
project.workDoneProgress_create(from, cbor_id, params_cb)
|
|
else blk: {
|
|
const params = try cbor.toJsonAlloc(self.allocator, params_cb);
|
|
defer self.allocator.free(params);
|
|
self.logger.print_err("lsp", "unsupported LSP request: {s} -> {s}", .{ method, params });
|
|
break :blk error.Unsupported;
|
|
};
|
|
}
|
|
|
|
fn persist_projects(self: *Process) void {
|
|
var i = self.projects.iterator();
|
|
while (i.next()) |p| self.persist_project(p.value_ptr.*) catch {};
|
|
}
|
|
|
|
fn persist_project(self: *Process, project: *Project) !void {
|
|
const no_persist = tp.env.get().is("no-persist");
|
|
if (no_persist and !project.persistent) return;
|
|
tp.trace(tp.channel.debug, .{ "persist_project", project.name });
|
|
self.logger.print("saving: {s}", .{project.name});
|
|
const file_name = try get_project_state_file_path(self.allocator, project);
|
|
defer self.allocator.free(file_name);
|
|
var file = try std.fs.createFileAbsolute(file_name, .{ .truncate = true });
|
|
defer file.close();
|
|
var buffer = std.io.bufferedWriter(file.writer());
|
|
defer buffer.flush() catch {};
|
|
try project.write_state(buffer.writer());
|
|
}
|
|
|
|
fn restore_project(self: *Process, project: *Project) !void {
|
|
tp.trace(tp.channel.debug, .{ "restore_project", project.name });
|
|
const file_name = try get_project_state_file_path(self.allocator, project);
|
|
defer self.allocator.free(file_name);
|
|
var file = std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }) catch |e| switch (e) {
|
|
error.FileNotFound => return,
|
|
else => return e,
|
|
};
|
|
defer file.close();
|
|
const stat = try file.stat();
|
|
var buffer = try self.allocator.alloc(u8, @intCast(stat.size));
|
|
defer self.allocator.free(buffer);
|
|
const size = try file.readAll(buffer);
|
|
try project.restore_state(buffer[0..size]);
|
|
}
|
|
|
|
fn get_project_state_file_path(allocator: std.mem.Allocator, project: *Project) ![]const u8 {
|
|
const path = project.name;
|
|
var stream = std.ArrayList(u8).init(allocator);
|
|
const writer = stream.writer();
|
|
_ = try writer.write(try root.get_state_dir());
|
|
_ = try writer.writeByte(std.fs.path.sep);
|
|
_ = try writer.write("projects");
|
|
_ = try writer.writeByte(std.fs.path.sep);
|
|
std.fs.makeDirAbsolute(stream.items) catch |e| switch (e) {
|
|
error.PathAlreadyExists => {},
|
|
else => return e,
|
|
};
|
|
for (path) |c| {
|
|
_ = if (std.fs.path.isSep(c))
|
|
try writer.write("__")
|
|
else if (c == ':')
|
|
try writer.write("___")
|
|
else
|
|
try writer.writeByte(c);
|
|
}
|
|
return stream.toOwnedSlice();
|
|
}
|
|
|
|
fn load_recent_projects(self: *Process, recent_projects: *std.ArrayList(RecentProject), project_directory: []const u8) !void {
|
|
var path = std.ArrayList(u8).init(self.allocator);
|
|
defer path.deinit();
|
|
const writer = path.writer();
|
|
_ = try writer.write(try root.get_state_dir());
|
|
_ = try writer.writeByte(std.fs.path.sep);
|
|
_ = try writer.write("projects");
|
|
|
|
var dir = try std.fs.cwd().openDir(path.items, .{ .iterate = true });
|
|
defer dir.close();
|
|
var iter = dir.iterate();
|
|
while (try iter.next()) |entry| {
|
|
if (entry.kind != .file) continue;
|
|
try self.read_project_name(path.items, entry.name, recent_projects, project_directory);
|
|
}
|
|
}
|
|
|
|
fn read_project_name(
|
|
self: *Process,
|
|
state_dir: []const u8,
|
|
file_path: []const u8,
|
|
recent_projects: *std.ArrayList(RecentProject),
|
|
project_directory: []const u8,
|
|
) !void {
|
|
var path = std.ArrayList(u8).init(self.allocator);
|
|
defer path.deinit();
|
|
const writer = path.writer();
|
|
_ = try writer.write(state_dir);
|
|
_ = try writer.writeByte(std.fs.path.sep);
|
|
_ = try writer.write(file_path);
|
|
|
|
var file = try std.fs.openFileAbsolute(path.items, .{ .mode = .read_only });
|
|
defer file.close();
|
|
const stat = try file.stat();
|
|
const buffer = try self.allocator.alloc(u8, @intCast(stat.size));
|
|
defer self.allocator.free(buffer);
|
|
_ = try file.readAll(buffer);
|
|
|
|
var iter: []const u8 = buffer;
|
|
var name: []const u8 = undefined;
|
|
if (cbor.matchValue(&iter, tp.extract(&name)) catch return) {
|
|
const last_used = if (std.mem.eql(u8, project_directory, name)) std.math.maxInt(@TypeOf(stat.mtime)) else stat.mtime;
|
|
(try recent_projects.addOne()).* = .{ .name = try self.allocator.dupe(u8, name), .last_used = last_used };
|
|
}
|
|
}
|
|
|
|
fn sort_projects_by_last_used(_: *Process, recent_projects: *std.ArrayList(RecentProject)) void {
|
|
const less_fn = struct {
|
|
fn less_fn(_: void, lhs: RecentProject, rhs: RecentProject) bool {
|
|
return lhs.last_used > rhs.last_used;
|
|
}
|
|
}.less_fn;
|
|
std.mem.sort(RecentProject, recent_projects.items, {}, less_fn);
|
|
}
|
|
};
|
|
|
|
fn request_path_files_async(a_: std.mem.Allocator, parent_: tp.pid_ref, project_: *Project, max_: usize, path_: []const u8) (SpawnError || std.fs.Dir.OpenError)!void {
|
|
return struct {
|
|
allocator: std.mem.Allocator,
|
|
project_name: []const u8,
|
|
path: []const u8,
|
|
parent: tp.pid,
|
|
max: usize,
|
|
dir: std.fs.Dir,
|
|
|
|
const path_files = @This();
|
|
const Receiver = tp.Receiver(*path_files);
|
|
|
|
fn spawn_link(allocator: std.mem.Allocator, parent: tp.pid_ref, project: *Project, max: usize, path: []const u8) (SpawnError || std.fs.Dir.OpenError)!void {
|
|
const self = try allocator.create(path_files);
|
|
self.* = .{
|
|
.allocator = allocator,
|
|
.project_name = try allocator.dupe(u8, project.name),
|
|
.path = try if (std.fs.path.isAbsolute(path))
|
|
allocator.dupe(u8, path)
|
|
else
|
|
std.fs.path.join(allocator, &[_][]const u8{ project.name, path }),
|
|
.parent = parent.clone(),
|
|
.max = max,
|
|
.dir = try std.fs.cwd().openDir(self.path, .{ .iterate = true }),
|
|
};
|
|
const pid = try tp.spawn_link(allocator, self, path_files.start, module_name ++ ".path_files");
|
|
pid.deinit();
|
|
}
|
|
|
|
fn start(self: *path_files) tp.result {
|
|
errdefer self.deinit();
|
|
const frame = tracy.initZone(@src(), .{ .name = "path_files scan" });
|
|
defer frame.deinit();
|
|
try self.parent.link();
|
|
self.iterate() catch |e| return tp.exit_error(e, @errorReturnTrace());
|
|
return tp.exit_normal();
|
|
}
|
|
|
|
fn deinit(self: *path_files) void {
|
|
self.dir.close();
|
|
self.allocator.free(self.path);
|
|
self.allocator.free(self.project_name);
|
|
self.parent.deinit();
|
|
}
|
|
|
|
fn iterate(self: *path_files) !void {
|
|
var count: usize = 0;
|
|
var iter = self.dir.iterateAssumeFirstIteration();
|
|
errdefer |e| self.parent.send(.{ "PRJ", "path_error", self.project_name, self.path, e }) catch {};
|
|
while (try iter.next()) |entry| {
|
|
switch (entry.kind) {
|
|
.directory => try self.parent.send(.{ "PRJ", "path_entry", self.project_name, self.path, "DIR", entry.name }),
|
|
.sym_link => try self.parent.send(.{ "PRJ", "path_entry", self.project_name, self.path, "LINK", entry.name }),
|
|
.file => try self.parent.send(.{ "PRJ", "path_entry", self.project_name, self.path, "FILE", entry.name }),
|
|
else => continue,
|
|
}
|
|
count += 1;
|
|
if (count >= self.max) break;
|
|
}
|
|
self.parent.send(.{ "PRJ", "path_done", self.project_name, self.path, count }) catch {};
|
|
}
|
|
}.spawn_link(a_, parent_, project_, max_, path_);
|
|
}
|
|
|
|
pub fn normalize_file_path(file_path: []const u8) []const u8 {
|
|
const project = tp.env.get().str("project");
|
|
if (project.len == 0) return file_path;
|
|
if (project.len >= file_path.len) return file_path;
|
|
if (std.mem.eql(u8, project, file_path[0..project.len]) and file_path[project.len] == std.fs.path.sep)
|
|
return file_path[project.len + 1 ..];
|
|
return file_path;
|
|
}
|