Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/project/lean/lean.ts
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { callback, delay } from "awaiting";6import * as lean_client from "lean-client-js-node";7import { isEqual } from "lodash";8import { EventEmitter } from "node:events";910import type { Client } from "@cocalc/project/client";11import { once } from "@cocalc/util/async-utils";12import { close } from "@cocalc/util/misc";13import { reuseInFlight } from "@cocalc/util/reuse-in-flight";1415type SyncString = any;16type LeanServer = any;1718// do not try to sync with lean more frequently than this19// unless it is completing quickly.20const SYNC_INTERVAL: number = 6000;2122// What lean has told us about a given file.23type Message = any;24type Messages = Message[];2526// What lean has told us about all files.27interface State {28tasks: any;29paths: { [key: string]: Messages };30}3132export class Lean extends EventEmitter {33paths: { [key: string]: SyncString } = {};34client: Client;35_server: LeanServer | undefined;36_state: State = { tasks: [], paths: {} };37private running: { [key: string]: number } = {};38dbg: Function;3940constructor(client: Client) {41super();42this.server = reuseInFlight(this.server.bind(this));43this.client = client;44this.dbg = this.client.dbg("LEAN SERVER");45this.running = {};46}4748close(): void {49this.kill();50close(this);51}5253is_running(path: string): boolean {54return !!this.running[path] && now() - this.running[path] < SYNC_INTERVAL;55}5657// nothing actually async in here... yet.58private async server(): Promise<LeanServer> {59if (this._server != undefined) {60if (this._server.alive()) {61return this._server;62}63// Kill cleans up any assumptions about stuff64// being sync'd.65this.kill();66// New server will now be created... below.67}68this._server = new lean_client.Server(69new lean_client.ProcessTransport(70"lean",71process.env.HOME ? process.env.HOME : ".", // satisfy typescript.72["-M 4096"],73),74);75this._server.error.on((err) => this.dbg("error:", err));76this._server.allMessages.on((allMessages) => {77this.dbg("messages: ", allMessages);7879const new_messages = {};80for (const x of allMessages.msgs) {81const path: string = x.file_name;82delete x.file_name;83if (new_messages[path] === undefined) {84new_messages[path] = [x];85} else {86new_messages[path].push(x);87}88}8990for (const path in this._state.paths) {91this.dbg("messages for ", path, new_messages[path]);92if (new_messages[path] === undefined) {93new_messages[path] = [];94}95this.dbg(96"messages for ",97path,98new_messages[path],99this._state.paths[path],100);101// length 0 is a special case needed when going from pos number of messages to none.102if (103new_messages[path].length === 0 ||104!isEqual(this._state.paths[path], new_messages[path])105) {106this.dbg("messages for ", path, "EMIT!");107this.emit("messages", path, new_messages[path]);108this._state.paths[path] = new_messages[path];109}110}111});112113this._server.tasks.on((currentTasks) => {114const { tasks } = currentTasks;115this.dbg("tasks: ", tasks);116const running = {};117for (const task of tasks) {118running[task.file_name] = true;119}120for (const path in running) {121const v: any[] = [];122for (const task of tasks) {123if (task.file_name === path) {124delete task.file_name; // no longer needed125v.push(task);126}127}128this.emit("tasks", path, v);129}130for (const path in this.running) {131if (!running[path]) {132this.dbg("server", path, " done; no longer running");133this.running[path] = 0;134this.emit("tasks", path, []);135if (this.paths[path].changed) {136// file changed while lean was running -- so run lean again.137this.dbg(138"server",139path,140" changed while running, so running again",141);142this.paths[path].on_change();143}144}145}146});147this._server.connect();148return this._server;149}150151// Start learn server parsing and reporting info about the given file.152// It will get updated whenever the file change.153async register(path: string): Promise<void> {154this.dbg("register", path);155if (this.paths[path] !== undefined) {156this.dbg("register", path, "already registered");157return;158}159// get the syncstring and start updating based on content160let syncstring: any = undefined;161while (syncstring == null) {162// todo change to be event driven!163syncstring = this.client.syncdoc({ path });164if (syncstring == null) {165await delay(1000);166} else if (syncstring.get_state() != "ready") {167await once(syncstring, "ready");168}169}170const on_change = async () => {171this.dbg("sync", path);172if (syncstring._closed) {173this.dbg("sync", path, "closed");174return;175}176if (this.is_running(path)) {177// already running, so do nothing - it will rerun again when done with current run.178this.dbg("sync", path, "already running");179this.paths[path].changed = true;180return;181}182183const value: string = syncstring.to_str();184if (this.paths[path].last_value === value) {185this.dbg("sync", path, "skipping sync since value did not change");186return;187}188if (value.trim() === "") {189this.dbg(190"sync",191path,192"skipping sync document is empty (and LEAN behaves weird in this case)",193);194this.emit("sync", path, syncstring.hash_of_live_version());195return;196}197this.paths[path].last_value = value;198this._state.paths[path] = [];199this.running[path] = now();200this.paths[path].changed = false;201this.dbg("sync", path, "causing server sync now");202await (await this.server()).sync(path, value);203this.emit("sync", path, syncstring.hash_of_live_version());204};205this.paths[path] = {206syncstring,207on_change,208};209syncstring.on("change", on_change);210if (!syncstring._closed) {211on_change();212}213syncstring.on("closed", () => {214this.unregister(path);215});216}217218// Stop updating given file on changes.219unregister(path: string): void {220this.dbg("unregister", path);221if (!this.paths[path]) {222// not watching it223return;224}225const x = this.paths[path];226delete this.paths[path];227delete this.running[path];228x.syncstring.removeListener("change", x.on_change);229x.syncstring.close();230}231232// Kill the lean server and unregister all paths.233kill(): void {234this.dbg("kill");235if (this._server != undefined) {236for (const path in this.paths) {237this.unregister(path);238}239this._server.dispose();240delete this._server;241}242}243244async restart(): Promise<void> {245this.dbg("restart");246if (this._server != undefined) {247for (const path in this.paths) {248this.unregister(path);249}250await this._server.restart();251}252}253254async info(255path: string,256line: number,257column: number,258): Promise<lean_client.InfoResponse> {259this.dbg("info", path, line, column);260if (!this.paths[path]) {261this.register(path);262await callback((cb) => this.once(`sync-#{path}`, cb));263}264return await (await this.server()).info(path, line, column);265}266267async complete(268path: string,269line: number,270column: number,271skipCompletions?: boolean,272): Promise<lean_client.CompleteResponse> {273this.dbg("complete", path, line, column);274if (!this.paths[path]) {275this.register(path);276await callback((cb) => this.once(`sync-#{path}`, cb));277}278const resp = await (279await this.server()280).complete(path, line, column, skipCompletions);281//this.dbg("complete response", path, line, column, resp);282return resp;283}284285async version(): Promise<string> {286return (await this.server()).getVersion();287}288289// Return state of parsing for everything that is currently registered.290state(): State {291return this._state;292}293294messages(path: string): any[] {295const x = this._state.paths[path];296if (x !== undefined) {297return x;298}299return [];300}301}302303let singleton: Lean | undefined;304305// Return the singleton lean instance. The client is assumed to never change.306export function lean_server(client: Client): Lean {307if (singleton === undefined) {308singleton = new Lean(client);309}310return singleton;311}312313function now(): number {314return Date.now();315}316317318