diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cb7b5e0f..2181e4bce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,9 @@ Changes to Calva. ## [Unreleased] - [Handling ansi code by stripping it](https://github.com/BetterThanTomorrow/calva/issues/696) -- Fix [Repl connection fails if afterCLJJackInCode errors](https://github.com/BetterThanTomorrow/calva/issues/703) +- Fix: [Output window sometimes getting out of synch, needing overwrite](https://github.com/BetterThanTomorrow/calva/issues/699) +- Fix: [The *Calva says* panel + output window opening at startup gets a bit too much](https://github.com/BetterThanTomorrow/calva/issues/702) +- Fix: [Repl connection fails if afterCLJJackInCode errors](https://github.com/BetterThanTomorrow/calva/issues/703) ## [2.0.110] - 2020-07-28 - [Fix Connect Fails on Windows](https://github.com/BetterThanTomorrow/calva/issues/694) diff --git a/src/connector.ts b/src/connector.ts index cf7c204ca..e59bbe14d 100644 --- a/src/connector.ts +++ b/src/connector.ts @@ -34,7 +34,6 @@ async function createAndConnectReplWindow(session: NReplSession, mode: "clj" | " async function connectToHost(hostname, port, connectSequence: ReplConnectSequence) { state.analytics().logEvent("REPL", "Connecting").send(); - let chan = state.outputChannel(); if (nClient) { nClient["silent"] = true; nClient.close(); @@ -44,32 +43,32 @@ async function connectToHost(hostname, port, connectSequence: ReplConnectSequenc util.setConnectingState(true); status.update(); try { - chan.appendLine("Hooking up nREPL sessions..."); + resultsOutput.appendToResultsDoc("; Hooking up nREPL sessions..."); // Create an nREPL client. waiting for the connection to be established. nClient = await NReplClient.create({ host: hostname, port: +port }) nClient.addOnCloseHandler(c => { util.setConnectedState(false); util.setConnectingState(false); if (!c["silent"]) // we didn't deliberately close this session, mention this fact. - chan.appendLine("nREPL Connection was closed"); + resultsOutput.appendToResultsDoc("; nREPL Connection was closed"); status.update(); }) cljSession = nClient.session; cljSession.replType = 'clj'; - chan.appendLine("Connected session: clj"); util.setConnectingState(false); util.setConnectedState(true); state.analytics().logEvent("REPL", "ConnectedCLJ").send(); state.cursor.set('clj', cljSession); state.cursor.set('cljc', cljSession); status.update(); - const outputDocument = await resultsOutput.openResultsDoc(true); + const outputDocument = await resultsOutput.openResultsDoc(); + resultsOutput.appendToResultsDoc(`; Connected session: clj\n${resultsOutput.CLJ_CONNECT_GREETINGS}`); resultsOutput.setSession(cljSession, nClient.ns); util.updateREPLSessionType(); // Initialize debugger await initializeDebugger(cljSession); - chan.appendLine('Debugger initialized'); + resultsOutput.appendToResultsDoc('; Debugger initialized'); await createAndConnectReplWindow(cljSession, "clj"); @@ -86,7 +85,7 @@ async function connectToHost(hostname, port, connectSequence: ReplConnectSequenc }); } catch (e) { - chan.appendLine("Evaluation of afterCLJReplJackInCode failed. See the output window.") + resultsOutput.appendToResultsDoc("; Evaluation of afterCLJReplJackInCode failed.") } } @@ -102,17 +101,16 @@ async function connectToHost(hostname, port, connectSequence: ReplConnectSequenc state.analytics().logEvent("REPL", "ConnectCljsRepl", isBuiltinType ? connectSequence.cljsType as string : "Custom").send(); } if (cljsSession) { - await setUpCljsRepl(cljsSession, chan, cljsBuild); + await setUpCljsRepl(cljsSession, cljsBuild); } - chan.appendLine('cljc files will use the clj REPL.' + (cljsSession ? ' (You can toggle this at will.)' : '')); } catch (e) { - chan.appendLine("Error while connecting cljs REPL: " + e); + resultsOutput.appendToResultsDoc("; Error while connecting cljs REPL: " + e); } status.update(); } catch (e) { util.setConnectingState(false); util.setConnectedState(false); - chan.appendLine("Failed connecting."); + resultsOutput.appendToResultsDoc("; Failed connecting."); state.analytics().logEvent("REPL", "FailedConnectingCLJ").send(); return false; } @@ -120,23 +118,22 @@ async function connectToHost(hostname, port, connectSequence: ReplConnectSequenc return true; } -async function setUpCljsRepl(session, chan, build) { +async function setUpCljsRepl(session, build) { state.cursor.set("cljs", session); status.update(); - chan.appendLine("Connected session: cljs" + (build ? ", repl: " + build : "")); + resultsOutput.appendToResultsDoc(`; Connected session: cljs${(build ? ", repl: " + build : "")}\n${resultsOutput.CLJS_CONNECT_GREETINGS}`); resultsOutput.setSession(session, 'cljs.user'); util.updateREPLSessionType(); createAndConnectReplWindow(session, "cljs"); } function getFigwheelMainBuilds() { - let chan = state.outputChannel(); let res = fs.readdirSync(state.getProjectRoot()); let builds = res.filter(x => x.match(/\.cljs\.edn/)).map(x => x.replace(/\.cljs\.edn$/, "")); if (builds.length == 0) { vscode.window.showErrorMessage("There are no figwheel build files (.cljs.edn) in the project directory."); - chan.appendLine("There are no figwheel build files (.cljs.edn) in the project directory."); - chan.appendLine("Connection to Figwheel Main aborted."); + resultsOutput.appendToResultsDoc("; There are no figwheel build files (.cljs.edn) in the project directory."); + resultsOutput.appendToResultsDoc("; Connection to Figwheel Main aborted."); throw "Aborted"; } return builds; @@ -225,14 +222,13 @@ function createCLJSReplType(cljsType: CljsTypeConfig, cljsTypeName: string, conn hasStarted = cljsType.isStarted, useDefaultBuild = true, startedBuilds: string[]; - const chan = state.outputChannel(), // The output processors are used to keep the user informed about the connection process // The output from Figwheel is meant for printing to the REPL prompt, // and since we print to Calva says we, only print some of the messages. - printThisPrinter: processOutputFn = x => { + const printThisPrinter: processOutputFn = x => { if (cljsType.printThisLineRegExp) { if (x.search(cljsType.printThisLineRegExp) >= 0) { - chan.appendLine(x.replace(/\s*$/, "")); + resultsOutput.appendToResultsDoc('; ' + x.replace(/\s*$/, "")); } } }, @@ -252,32 +248,32 @@ function createCLJSReplType(cljsType: CljsTypeConfig, cljsTypeName: string, conn // When the app is ready to start, say so. if (!haveShownStartMessage && cljsType.isReadyToStartRegExp) { if (x.search(cljsType.isReadyToStartRegExp) >= 0) { - chan.appendLine("CLJS REPL ready to connect. Please, start your ClojureScript app."); + resultsOutput.appendToResultsDoc("; CLJS REPL ready to connect. Please, start your ClojureScript app."); haveShownStartMessage = true; } } // If we have an appURL to go with the ”start now” message, say so if (appURL && haveShownStartMessage && !haveShownAppURL) { if (cljsType.shouldOpenUrl) { - chan.appendLine(` Opening ClojureScript app in the browser at: ${appURL} ...`); + resultsOutput.appendToResultsDoc(`; Opening ClojureScript app in the browser at: ${appURL} ...`); open(appURL).catch(reason => { - chan.appendLine("Error opening ClojureScript app in the browser: " + reason); + resultsOutput.appendToResultsDoc("; Error opening ClojureScript app in the browser: " + reason); }); } else { - chan.appendLine(" Open the app on this URL: " + appURL); + resultsOutput.appendToResultsDoc("; Open the app on this URL: " + appURL); } haveShownAppURL = true; } // Wait for any appURL to be printed before we round of the ”start now” message. // (If we do not have the regexp for extracting the appURL, do not wait for appURL.) if (!haveShownStartSuffix && (haveShownAppURL || (haveShownStartMessage && !cljsType.openUrlRegExp))) { - chan.appendLine(" The CLJS REPL will connect when your app is running."); + resultsOutput.appendToResultsDoc("; The CLJS REPL will connect when your app is running."); haveShownStartSuffix = true; } }, // This processor prints everything. We use it for stderr below. allPrinter: processOutputFn = x => { - chan.appendLine(util.stripAnsi(x).replace(/\s*$/, "")); + resultsOutput.appendToResultsDoc('; ' + util.stripAnsi(x).replace(/\s*$/, "")); } let replType: ReplType = { @@ -346,7 +342,7 @@ function createCLJSReplType(cljsType: CljsTypeConfig, cljsTypeName: string, conn }); } if (builds) { - chan.appendLine("Starting cljs repl for: " + projectTypeName + "..."); + resultsOutput.appendToResultsDoc("; Starting cljs repl for: " + projectTypeName + "..."); state.extensionContext.workspaceState.update('cljsReplTypeHasBuilds', true); startCode = startCode.replace("%BUILDS%", builds.map(x => { return `"${x}"` }).join(" ")); const result = evalConnectCode(session, startCode, name, checkFn, [startAppNowProcessor, printThisPrinter], [allPrinter]); @@ -355,11 +351,11 @@ function createCLJSReplType(cljsType: CljsTypeConfig, cljsTypeName: string, conn } return result; } else { - chan.appendLine("Aborted starting cljs repl."); + resultsOutput.appendToResultsDoc("; Aborted starting cljs repl."); throw "Aborted"; } } else { - chan.appendLine("Starting cljs repl for: " + projectTypeName + "..."); + resultsOutput.appendToResultsDoc("; Starting cljs repl for: " + projectTypeName + "..."); return evalConnectCode(session, startCode, name, checkFn, [startAppNowProcessor, printThisPrinter], [allPrinter]); } } else { @@ -387,23 +383,20 @@ function createCLJSReplType(cljsType: CljsTypeConfig, cljsTypeName: string, conn } async function makeCljsSessionClone(session, repl: ReplType, projectTypeName: string) { - let chan = state.outputChannel(); - - chan.appendLine("Creating cljs repl session..."); + resultsOutput.appendToResultsDoc("; Creating cljs repl session..."); let newCljsSession = await session.clone(); newCljsSession.replType = 'cljs'; if (newCljsSession) { - chan.show(true); - chan.appendLine("Connecting cljs repl: " + projectTypeName + "..."); - chan.appendLine("The Calva Connection Log might have more connection progress information."); + resultsOutput.appendToResultsDoc("; Connecting cljs repl: " + projectTypeName + "..."); + resultsOutput.appendToResultsDoc("; The Calva Connection Log might have more connection progress information."); if (repl.start != undefined) { if (await repl.start(newCljsSession, repl.name, repl.started)) { state.analytics().logEvent("REPL", "StartedCLJS", repl.name).send(); - chan.appendLine("Cljs builds started"); + resultsOutput.appendToResultsDoc("; Cljs builds started"); newCljsSession = await session.clone(); } else { state.analytics().logEvent("REPL", "FailedStartingCLJS", repl.name).send(); - chan.appendLine("Failed starting cljs repl"); + resultsOutput.appendToResultsDoc("; Failed starting cljs repl"); state.cursor.set('cljsBuild', null); return [null, null]; } @@ -416,7 +409,7 @@ async function makeCljsSessionClone(session, repl: ReplType, projectTypeName: st let build = state.deref().get('cljsBuild') state.analytics().logEvent("REPL", "FailedConnectingCLJS", repl.name).send(); let failed = "Failed starting cljs repl" + (build != null ? ` for build: ${build}. Is the build running and connected?\n See the Output channel "Calva Connection Log" for any hints on what went wrong.` : ""); - chan.appendLine(`${failed}`); + resultsOutput.appendToResultsDoc(`; ${failed}`); state.cursor.set('cljsBuild', null); vscode.window.showInformationMessage( failed, @@ -433,8 +426,7 @@ async function makeCljsSessionClone(session, repl: ReplType, projectTypeName: st } async function promptForNreplUrlAndConnect(port, connectSequence: ReplConnectSequence) { - let current = state.deref(), - chan = state.outputChannel(); + let current = state.deref(); let url = await vscode.window.showInputBox({ placeHolder: "Enter existing nREPL hostname:port here...", @@ -451,7 +443,7 @@ async function promptForNreplUrlAndConnect(port, connectSequence: ReplConnectSeq state.cursor.set("port", parsedPort); await connectToHost(hostname, parsedPort, connectSequence); } else { - chan.appendLine("Bad url: " + url); + resultsOutput.appendToResultsDoc("; Bad url: " + url); util.setConnectingState(false); status.update(); } @@ -467,8 +459,7 @@ export let cljSession: NReplSession; export let cljsSession: NReplSession; export async function connect(connectSequence: ReplConnectSequence, isAutoConnect, isJackIn) { - const chan = state.outputChannel(), - cljsTypeName = projectTypes.getCljsTypeName(connectSequence); + const cljsTypeName = projectTypes.getCljsTypeName(connectSequence); state.analytics().logEvent("REPL", "ConnectInitiated", isAutoConnect ? "auto" : "manual"); state.analytics().logEvent("REPL", "ConnectInitiated", cljsTypeName).send(); @@ -489,7 +480,7 @@ export async function connect(connectSequence: ReplConnectSequence, isAutoConnec await promptForNreplUrlAndConnect(port, connectSequence); } } else { - chan.appendLine('No nrepl port file found. (Calva does not start the nrepl for you, yet.)'); + resultsOutput.appendToResultsDoc('; No nrepl port file found. (Calva does not start the nrepl for you, yet.)'); await promptForNreplUrlAndConnect(port, connectSequence); } } else { @@ -498,26 +489,24 @@ export async function connect(connectSequence: ReplConnectSequence, isAutoConnec return true; } -function standaloneConnect(connectSequence: ReplConnectSequence, chan: vscode.OutputChannel) { +function standaloneConnect(connectSequence: ReplConnectSequence) { if (connectSequence) { const cljsTypeName = projectTypes.getCljsTypeName(connectSequence); - chan.appendLine(`Connecting ...`); + resultsOutput.appendToResultsDoc(`; Connecting ...`); state.analytics().logEvent("REPL", "StandaloneConnect", `${connectSequence.name} + ${cljsTypeName}`).send(); connect(connectSequence, false, false).catch(() => { }); } else { - chan.appendLine("Aborting connect, error determining connect sequence."); + resultsOutput.appendToResultsDoc("; Aborting connect, error determining connect sequence."); } } export default { connectNonProjectREPLCommand: async () => { - const chan = state.outputChannel(); const connectSequence = await askForConnectSequence(projectTypes.getAllProjectTypes(), 'connect-type', "ConnectInterrupted"); - standaloneConnect(connectSequence, chan); + standaloneConnect(connectSequence); }, connectCommand: async () => { - const chan = state.outputChannel(); // TODO: Figure out a better way to have an initialized project directory. try { await state.initProjectDir(); @@ -528,7 +517,7 @@ export default { } const cljTypes = await projectTypes.detectProjectTypes(), connectSequence = await askForConnectSequence(cljTypes, 'connect-type', "ConnectInterrupted"); - standaloneConnect(connectSequence, chan); + standaloneConnect(connectSequence); }, disconnect: (options = null, callback = () => { }) => { ['clj', 'cljs'].forEach(sessionType => { @@ -570,15 +559,14 @@ export default { } }, switchCljsBuild: async () => { - let cljSession = util.getSession('clj'), - chan = state.outputChannel(); + let cljSession = util.getSession('clj'); const cljsTypeName: string = state.extensionContext.workspaceState.get('selectedCljsTypeName'), cljTypeName: string = state.extensionContext.workspaceState.get('selectedCljTypeName'); state.analytics().logEvent("REPL", "switchCljsBuild", cljsTypeName).send(); let [session, build] = await makeCljsSessionClone(cljSession, translatedReplType, cljTypeName); if (session) { - await setUpCljsRepl(session, chan, build); + await setUpCljsRepl(session, build); } status.update(); } diff --git a/src/extension.ts b/src/extension.ts index 5acc01cde..9e3ad50e8 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -122,7 +122,7 @@ function activate(context: vscode.ExtensionContext) { chan.appendLine("Calva activated."); - + outputWindow.initResultsDoc(); status.update(); // COMMANDS @@ -233,8 +233,6 @@ function activate(context: vscode.ExtensionContext) { } } - chan.appendLine("Start the REPL with the command *Start Project REPL and connect (aka Jack-in)*.") - chan.appendLine("Default keybinding for Jack-in: ctrl+alt+c ctrl+alt+j"); state.analytics().logPath("/activated").logEvent("LifeCycle", "Activated").send(); if (!cwExtension) { diff --git a/src/result-output.ts b/src/result-output.ts index 0e7b02b41..452cf561f 100644 --- a/src/result-output.ts +++ b/src/result-output.ts @@ -1,4 +1,3 @@ -import * as os from 'os'; import * as path from 'path'; import * as vscode from 'vscode'; import * as state from './state'; @@ -9,16 +8,23 @@ import * as util from './utilities'; export const REPL_FILE_EXT = "repl-file" const RESULTS_DOC_NAME = `output.${REPL_FILE_EXT}`; -const TIPS = ['; The keyboard shortcut `ctrl+alt+c o` shows and focuses this window.', - '; You can edit the contents here. Use it as a REPL if you like.\n\ +const START_GREETINGS = '; This is the Calva evaluation results output window.\n\ +; Leave it open, please. Because quirks.\n\ +; The keyboard shortcut `ctrl+alt+c o` shows and focuses this window.\n\ +; Please see https://calva.io/output/ for more info.\n\n\ +; Start the REPL with the command *Start Project REPL and connect (aka Jack-in)*.\n\ +; Default keybinding for REPL Jack-in: `ctrl+alt+c ctrl+alt+j`\n\ +; Or connect to a running REPL using `ctrl+alt+c ctrl+alt+c`\n\ +; Happy coding! ♥️'; + +export const CLJ_CONNECT_GREETINGS = '; You can edit the contents here. Use it as a REPL if you like.\n\ ; Use `alt+enter` to evaluate the current top level form.\n\ -; (`ctrl+enter` evaluates the current form.)', - '; File URLs in stacktrace frames are peekable and clickable.', - '; In ClojureScript projects, use the command *Calva: Toggle REPL connection* to choose which REPL to use (clj or cljs).']; +; (`ctrl+enter` evaluates the current form.)\n\ +; File URLs in stacktrace frames are peekable and clickable.'; -const GREETINGS = ['; This is the Calva evaluation results output window.\n\ -; Leave it open, please. Because quirks.', - '; Please see https://calva.io/output/ for docs. Happy coding!\n']; +export const CLJS_CONNECT_GREETINGS = '; You can choose which REPL to use (clj or cljs):\n\ +; *Calva: Toggle REPL connection*\n\ +; (There is a button in the status bar for this)'; const OUTPUT_FILE_DIR = () => path.join(state.getProjectRoot(), '.calva', 'output-window'); const DOC_URI = () => vscode.Uri.file(path.join(OUTPUT_FILE_DIR(), RESULTS_DOC_NAME)); @@ -78,29 +84,44 @@ function writeTextToFile(uri: vscode.Uri, text: string): Thenable { return vscode.workspace.fs.writeFile(uri, ui8a); } -export async function openResultsDoc(init: boolean = false): Promise { - init = init && !_prompt; - if (init) { - const kondoPath = path.join(OUTPUT_FILE_DIR(), '.clj-kondo') - await vscode.workspace.fs.createDirectory(vscode.Uri.file(kondoPath)); - await writeTextToFile(vscode.Uri.file(path.join(kondoPath, 'config.edn')), "^:replace {:linters {}}") - const greetings = `${GREETINGS[0]}\n${TIPS[Math.floor(Math.random() * TIPS.length)]}\n${GREETINGS[1]}\n`; - await vscode.workspace.fs.createDirectory(vscode.Uri.file(OUTPUT_FILE_DIR())); - await writeTextToFile(DOC_URI(), greetings); +export async function initResultsDoc(): Promise { + await state.initProjectDir(); + const kondoPath = path.join(OUTPUT_FILE_DIR(), '.clj-kondo') + await vscode.workspace.fs.createDirectory(vscode.Uri.file(kondoPath)); + await writeTextToFile(vscode.Uri.file(path.join(kondoPath, 'config.edn')), "^:replace {:linters {}}"); + + await vscode.workspace.fs.createDirectory(vscode.Uri.file(OUTPUT_FILE_DIR())); + let resultsDoc: vscode.TextDocument; + try { + resultsDoc = await vscode.workspace.openTextDocument(DOC_URI()); + } catch (e) { + await writeTextToFile(DOC_URI(), ''); + resultsDoc = await vscode.workspace.openTextDocument(DOC_URI()); } + const greetings = `${START_GREETINGS}\n\n`; + const edit = new vscode.WorkspaceEdit(); + const fullRange = new vscode.Range(resultsDoc.positionAt(0), resultsDoc.positionAt(Infinity)); + edit.replace(DOC_URI(), fullRange, greetings); + const success = await vscode.workspace.applyEdit(edit); + resultsDoc.save(); + + const resultsEditor = await vscode.window.showTextDocument(resultsDoc, getViewColumn(), true); + const firstPos = resultsEditor.document.positionAt(0); + const lastPos = resultsDoc.positionAt(Infinity); + resultsEditor.selection = new vscode.Selection(lastPos, lastPos); + resultsEditor.revealRange(new vscode.Range(firstPos, firstPos)); + // For some reason onDidChangeTextEditorViewColumn won't fire + state.extensionContext.subscriptions.push(vscode.window.onDidChangeActiveTextEditor(event => { + if (isResultsDoc(event.document)) { + setViewColumn(event.viewColumn); + } + })); + return resultsDoc; +} + +export async function openResultsDoc(): Promise { const resultsDoc = await vscode.workspace.openTextDocument(DOC_URI()); - const resultsEditor = await vscode.window.showTextDocument(resultsDoc, getViewColumn(), !init); - if (init) { - vscode.commands.executeCommand('workbench.action.files.revert'); - const firstPos = resultsEditor.document.positionAt(0); - resultsEditor.revealRange(new vscode.Range(firstPos, firstPos)); - // For some reason onDidChangeTextEditorViewColumn won't fire - state.extensionContext.subscriptions.push(vscode.window.onDidChangeActiveTextEditor(event => { - if (isResultsDoc(event.document)) { - setViewColumn(event.viewColumn); - } - })); - } + const resultsEditor = await vscode.window.showTextDocument(resultsDoc, getViewColumn(), false); return resultsDoc; }