diff --git a/src/nrepl/events.ts b/src/nrepl/events.ts new file mode 100644 index 000000000..08f7e4a22 --- /dev/null +++ b/src/nrepl/events.ts @@ -0,0 +1,13 @@ +interface Event { + type: string +} + +export class NReplEvaluationStartedEvent implements Event { + type = "started" + constructor(public fileName: string, public filePath: string){} +} + +export class NReplEvaluationFinishedEvent implements Event { + type = "finished"; + constructor(public fileName: string, public filePath: string, public error?:string){} +} diff --git a/src/nrepl/index.ts b/src/nrepl/index.ts index 702b8b457..5d0f1e489 100644 --- a/src/nrepl/index.ts +++ b/src/nrepl/index.ts @@ -1,3 +1,4 @@ +import { Event, EventEmitter } from "vscode"; import * as net from "net"; import { BEncoderStream, BDecoderStream } from "./bencode"; import * as state from './../state'; @@ -5,6 +6,10 @@ import * as replWindow from './../repl-window'; import * as util from '../utilities'; import { prettyPrint } from '../../out/cljs-lib/cljs-lib'; import { PrettyPrintingOptions, disabledPrettyPrinter, getServerSidePrinter } from "../printer"; +import { NReplEvaluationStartedEvent, NReplEvaluationFinishedEvent } from "./events"; + +const eventEmitter = new EventEmitter(); +export const onNReplEvent = eventEmitter.event; /** An nRREPL client */ export class NReplClient { @@ -29,6 +34,7 @@ export class NReplClient { ns: string = "user"; private constructor(socket: net.Socket) { + // TODO: Emit client connection events this.socket = socket; this.socket.on("error", e => { console.error(e); @@ -305,14 +311,27 @@ export class NReplSession { stderr?: (x: string) => void, stdout?: (x: string) => void, pprintOptions: PrettyPrintingOptions - } = { + } = { pprintOptions: disabledPrettyPrinter }) { + eventEmitter.fire(new NReplEvaluationStartedEvent(opts.fileName, opts.filePath)); + + const fireEventOnFin = ( + finalize: ((response: string) => any), + createEvent: ((response: string) => any) + ) => (response: string) => { + eventEmitter.fire(createEvent(response)); + finalize(response); + } + let id = this.client.nextId; let evaluation = new NReplEvaluation(id, this, opts.stderr, opts.stdout, null, new Promise((resolve, reject) => { this.messageHandlers[id] = (msg) => { - evaluation.setHandlers(resolve, reject); + evaluation.setHandlers( + fireEventOnFin(resolve, _ => new NReplEvaluationFinishedEvent(opts.fileName, opts.filePath)), + fireEventOnFin(reject, (reason: string) => new NReplEvaluationFinishedEvent(opts.fileName, opts.filePath, reason)) + ); if (evaluation.onMessage(msg, opts.pprintOptions)) { return true; } @@ -762,4 +781,4 @@ export class NReplEvaluation { }); return (num); } -} \ No newline at end of file +} diff --git a/src/statusbar/cljsBuildStatusBar.ts b/src/statusbar/cljs-build.ts similarity index 97% rename from src/statusbar/cljsBuildStatusBar.ts rename to src/statusbar/cljs-build.ts index abfcfe7f3..537c02239 100644 --- a/src/statusbar/cljsBuildStatusBar.ts +++ b/src/statusbar/cljs-build.ts @@ -2,7 +2,7 @@ import { window, StatusBarAlignment, StatusBarItem } from "vscode"; import * as state from '../state'; import * as util from '../utilities'; -export class CljsBuildStatusBar { +export class CljsBuildStatusBarItem { private statusBarItem: StatusBarItem; constructor(alignment: StatusBarAlignment) { this.statusBarItem = window.createStatusBarItem(alignment); diff --git a/src/statusbar/connectionStatusBar.ts b/src/statusbar/connection.ts similarity index 97% rename from src/statusbar/connectionStatusBar.ts rename to src/statusbar/connection.ts index f2eacae6c..370b6287b 100644 --- a/src/statusbar/connectionStatusBar.ts +++ b/src/statusbar/connection.ts @@ -3,7 +3,7 @@ import configReader from "../configReader"; import * as state from '../state'; import * as util from '../utilities'; -export class ConnectionStatusBar { +export class ConnectionStatusBarItem { private statusBarItem: StatusBarItem; constructor(alignment: StatusBarAlignment) { diff --git a/src/statusbar/file-type.ts b/src/statusbar/file-type.ts new file mode 100644 index 000000000..6e29ed74d --- /dev/null +++ b/src/statusbar/file-type.ts @@ -0,0 +1,143 @@ +import { window, StatusBarAlignment, StatusBarItem } from "vscode"; +import { activeReplWindow } from '../repl-window'; + +import { onNReplEvent } from "../nrepl"; +import { NReplEvaluationStartedEvent, NReplEvaluationFinishedEvent } from "../nrepl/events"; + +import configReader from "../configReader"; +import * as state from '../state'; +import * as util from '../utilities'; + +export interface EvaluationError { + fileName: string; + filePath: string; + message: string; +} + +export class FileTypeStatusBarItem { + private statusBarItem: StatusBarItem; + + private activeEvals = new Array(); + + private errors = new Array(); + + private get isEvaluating() { + return this.activeEvals.length > 0; + } + + private get hasErrors(){ + return this.errors.length > 0; + } + + constructor(alignment: StatusBarAlignment) { + this.statusBarItem = window.createStatusBarItem(alignment); + // TODO: Event handling and resulting state should be moved to global state + onNReplEvent(this.handleNreplEvent); + } + + update() { + const connected = state.deref().get("connected"); + const doc = util.getDocument({}); + const fileType = util.getFileType(doc); + const sessionType = util.getREPLSessionType(); + + let command = null; + let text = "Disconnected"; + let tooltip = "No active REPL session"; + let color = configReader.colors.disconnected; + + if(connected) { + if (fileType == 'cljc' && sessionType !== null && !activeReplWindow()) { + text = this.statusTextDecorator("cljc/" + sessionType); + if (util.getSession('clj') !== null && util.getSession('cljs') !== null) { + command = "calva.toggleCLJCSession"; + tooltip = `Click to use ${(sessionType === 'clj' ? 'cljs' : 'clj')} REPL for cljc`; + } + } else if (sessionType === 'cljs') { + text = this.statusTextDecorator("cljs"); + tooltip = "Connected to ClojureScript REPL"; + } else if (sessionType === 'clj') { + text = this.statusTextDecorator("clj"); + tooltip = "Connected to Clojure REPL"; + } + color = this.connectedStatusColor(); + if(this.hasErrors) { + tooltip = this.errorTooltip(); + } + } + + this.statusBarItem.command = command; + this.statusBarItem.text = text; + this.statusBarItem.tooltip = tooltip; + this.statusBarItem.color = color + this.statusBarItem.show(); + } + + private statusTextDecorator(text): string { + if(this.hasErrors) { + const c = this.errors.length; + text = `${text} $(alert) ${c > 1 ? c : ""}` + } + if(this.isEvaluating) { + text = text + " $(gear~spin)"; + } + return text; + } + + private connectedStatusColor(): string { + const c = configReader.colors; + if(this.hasErrors) { + return c.error; + } else if (this.isEvaluating) { + return c.launching; + } + return c.typeStatus; + } + + private errorTooltip(): string { + if(this.errors.length > 1){ + return "There are errors in multiple files"; + } + const err = this.errors[0]; + return `Error: ${err.fileName}: ${err.message}`; + } + + private handleNreplEvent = (e: NReplEvaluationStartedEvent | NReplEvaluationFinishedEvent) => { + switch(e.type){ + case "started": + this.removeError(e.filePath); + this.activeEvals.push(e.filePath); + break; + case "finished": + this.removeActive(e.filePath); + const fe = e; + if(fe.error) { + this.errors.push({ + fileName: fe.fileName, + filePath: fe.filePath, + message: fe.error + }); + } + break; + } + this.update(); + } + + private removeActive(filePath: string) { + const idx = this.activeEvals.indexOf(filePath); + if(idx !== -1) { + this.activeEvals.splice(idx, 1); + } + } + + private removeError(filePath: string) { + const idx = this.errors.findIndex(e => e.filePath === filePath); + if(idx !== -1) { + this.errors.splice(idx, 1); + } + } + + dispose() { + this.statusBarItem.dispose(); + } +} diff --git a/src/statusbar/index.ts b/src/statusbar/index.ts index e76628a94..7df827f7e 100644 --- a/src/statusbar/index.ts +++ b/src/statusbar/index.ts @@ -1,16 +1,16 @@ import { StatusBarAlignment } from "vscode"; -import { TypeStatusBar } from "./typeStatusBar"; -import { PrettyPrintStatusBar } from "./prettyPrintStatusBar"; -import { CljsBuildStatusBar } from "./cljsBuildStatusBar"; -import { ConnectionStatusBar } from "./connectionStatusBar"; +import { FileTypeStatusBarItem } from "./file-type"; +import { PrettyPrintStatusBarItem } from "./pretty-print"; +import { CljsBuildStatusBarItem } from "./cljs-build"; +import { ConnectionStatusBarItem } from "./connection"; const statusBarItems = []; function init(): any[] { - statusBarItems.push(new ConnectionStatusBar(StatusBarAlignment.Left)); - statusBarItems.push(new TypeStatusBar(StatusBarAlignment.Left)); - statusBarItems.push(new CljsBuildStatusBar(StatusBarAlignment.Left)); - statusBarItems.push(new PrettyPrintStatusBar(StatusBarAlignment.Right)); + statusBarItems.push(new ConnectionStatusBarItem(StatusBarAlignment.Left)); + statusBarItems.push(new FileTypeStatusBarItem(StatusBarAlignment.Left)); + statusBarItems.push(new CljsBuildStatusBarItem(StatusBarAlignment.Left)); + statusBarItems.push(new PrettyPrintStatusBarItem(StatusBarAlignment.Right)); update(); return statusBarItems; } diff --git a/src/statusbar/prettyPrintStatusBar.ts b/src/statusbar/pretty-print.ts similarity index 95% rename from src/statusbar/prettyPrintStatusBar.ts rename to src/statusbar/pretty-print.ts index ad0b6d21a..b076b8b26 100644 --- a/src/statusbar/prettyPrintStatusBar.ts +++ b/src/statusbar/pretty-print.ts @@ -2,7 +2,7 @@ import { window, StatusBarAlignment, StatusBarItem } from "vscode"; import configReader from "../configReader"; import * as state from '../state'; -export class PrettyPrintStatusBar { +export class PrettyPrintStatusBarItem { private statusBarItem: StatusBarItem; constructor(alignment: StatusBarAlignment) { this.statusBarItem = window.createStatusBarItem(alignment); diff --git a/src/statusbar/typeStatusBar.ts b/src/statusbar/typeStatusBar.ts deleted file mode 100644 index 5a46b2e2d..000000000 --- a/src/statusbar/typeStatusBar.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { window, StatusBarAlignment, StatusBarItem } from "vscode"; -import { activeReplWindow } from '../repl-window'; -import configReader from "../configReader"; -import * as state from '../state'; -import * as util from '../utilities'; - -export class TypeStatusBar { - private statusBarItem: StatusBarItem; - constructor(alignment: StatusBarAlignment) { - this.statusBarItem = window.createStatusBarItem(alignment); - } - - update() { - const connected = state.deref().get("connected"); - const doc = util.getDocument({}); - const fileType = util.getFileType(doc); - const sessionType = util.getREPLSessionType(); - - let command = null; - let text = "Disconnected"; - let tooltip = "No active REPL session"; - let color = configReader.colors.disconnected; - - if(connected) { - if (fileType == 'cljc' && sessionType !== null && !activeReplWindow()) { - text = "cljc/" + sessionType; - if (util.getSession('clj') !== null && util.getSession('cljs') !== null) { - command = "calva.toggleCLJCSession"; - tooltip = `Click to use ${(sessionType === 'clj' ? 'cljs' : 'clj')} REPL for cljc`; - } - } else if (sessionType === 'cljs') { - text = "cljs"; - tooltip = "Connected to ClojureScript REPL"; - } else if (sessionType === 'clj') { - text = "clj"; - tooltip = "Connected to Clojure REPL"; - } - color = configReader.colors.typeStatus; - } - - this.statusBarItem.command = command; - this.statusBarItem.text = text; - this.statusBarItem.tooltip = tooltip; - this.statusBarItem.color = color - this.statusBarItem.show(); - } - - dispose() { - this.statusBarItem.dispose(); - } -}