diff --git a/.vscode/settings.json b/.vscode/settings.json index 0a68728f1..bc7827e7e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,6 +13,8 @@ "Insourcing", "Jackin", "Jackout", + "Libspec", + "Notespace", "OVSX", "Pseudoterminal", "REBL", @@ -66,6 +68,7 @@ "entrypoint", "errored", "eval", + "evals", "falsesomething", "fehse", "feldman", @@ -120,6 +123,7 @@ "sbin", "scaturro", "schäfer", + "seqs", "sivertsen", "stacktraces", "stian", diff --git a/CHANGELOG.md b/CHANGELOG.md index ee30cad39..d0c9f5f10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ Changes to Calva. ## [Unreleased] +- Fix [Connect ”not in project” glitches](https://github.com/BetterThanTomorrow/calva/issues/814) ## [2.0.171] - 2021-02-10 - Update clojure-lsp to version 2021.02.09-18.28.06 (Fix: [Auto completion does not work in clojure-lsp only mode (no repl connection)](https://github.com/BetterThanTomorrow/calva/issues/996#issuecomment-776148282)) diff --git a/package.json b/package.json index 18475f585..9254c53a3 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "onLanguage:clojure", "onCommand:calva.startNewRepl", "onCommand:calva.jackIn", - "onCommand:calva.jackInOrConnect", + "onCommand:calva.startOrConnectRepl", "onCommand:calva.connect", "onCommand:calva.connectNonProjectREPL", "workspaceContains:**/project.clj", @@ -658,14 +658,8 @@ "category": "Calva" }, { - "command": "calva.jackInOrConnect", - "title": "Jack-in or Connect to REPL Server", - "enablement": "!calva:connected && !calva:connecting", - "category": "Calva" - }, - { - "command": "calva.startNewRepl", - "title": "Start a Clojure REPL", + "command": "calva.startOrConnectRepl", + "title": "Start or Connect to a Clojure REPL", "category": "Calva" }, { @@ -1241,7 +1235,7 @@ "when": "calva:keybindingsEnabled" }, { - "command": "calva.startNewRepl", + "command": "calva.startOrConnectRepl", "key": "ctrl+alt+c ctrl+alt+r" }, { @@ -1724,11 +1718,7 @@ "menus": { "commandPalette": [ { - "command": "calva.jackInOrConnect", - "when": "false" - }, - { - "command": "calva.startNewRepl", + "command": "calva.startOrConnectRepl", "when": "true" }, { @@ -1767,7 +1757,7 @@ { "when": "editorLangId == clojure", "enablement": "!calva:connected", - "command": "calva.jackInOrConnect", + "command": "calva.startOrConnectRepl", "group": "calva/x-connect" }, { diff --git a/src/connector.ts b/src/connector.ts index fb27de480..c1dca12e1 100644 --- a/src/connector.ts +++ b/src/connector.ts @@ -452,8 +452,13 @@ export async function connect(connectSequence: ReplConnectSequence, try { if (port === undefined) { - let bytes = await vscode.workspace.fs.readFile(portFile); - port = new TextDecoder("utf-8").decode(bytes); + try { + await vscode.workspace.fs.stat(portFile); + let bytes = await vscode.workspace.fs.readFile(portFile); + port = new TextDecoder("utf-8").decode(bytes); + } catch { + throw new Error("No nrepl port found"); + } } if (port) { hostname = hostname !== undefined ? hostname : "localhost"; @@ -476,7 +481,13 @@ export async function connect(connectSequence: ReplConnectSequence, } -async function standaloneConnect(connectSequence: ReplConnectSequence) { +async function standaloneConnect(context: vscode.ExtensionContext, connectSequence: ReplConnectSequence) { + await state.initProjectDir(); + let projectDirUri = state.getProjectRootUri(); + if (!projectDirUri) { + projectDirUri = await state.getOrCreateNonProjectRoot(context); + } + await state.initProjectDir(projectDirUri); await outputWindow.initResultsDoc(); await outputWindow.openResultsDoc(); @@ -492,12 +503,12 @@ async function standaloneConnect(connectSequence: ReplConnectSequence) { } export default { - connectNonProjectREPLCommand: async () => { + connectNonProjectREPLCommand: async (context: vscode.ExtensionContext) => { status.updateNeedReplUi(true); const connectSequence = await askForConnectSequence(projectTypes.getAllProjectTypes(), 'connect-type', "ConnectInterrupted"); - standaloneConnect(connectSequence); + standaloneConnect(context, connectSequence); }, - connectCommand: async () => { + connectCommand: async (context: vscode.ExtensionContext) => { status.updateNeedReplUi(true); // TODO: Figure out a better way to have an initialized project directory. try { @@ -505,12 +516,12 @@ export default { await liveShareSupport.setupLiveShareListener(); } catch { // Could be a bae file, user makes the call - vscode.commands.executeCommand('calva.jackInOrConnect'); + vscode.commands.executeCommand('calva.startOrConnectRepl'); return; } const cljTypes = await projectTypes.detectProjectTypes(), connectSequence = await askForConnectSequence(cljTypes, 'connect-type', "ConnectInterrupted"); - standaloneConnect(connectSequence); + standaloneConnect(context, connectSequence); }, disconnect: (options = null, callback = () => { }) => { status.updateNeedReplUi(false); diff --git a/src/extension.ts b/src/extension.ts index 12fc0f7d6..80f39c94d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -133,11 +133,13 @@ async function activate(context: vscode.ExtensionContext) { status.update(context); // COMMANDS - context.subscriptions.push(vscode.commands.registerCommand('calva.jackInOrConnect', jackIn.calvaJackInOrConnect)); - context.subscriptions.push(vscode.commands.registerCommand('calva.startNewRepl', jackIn.calvaJackInOrConnect)); + context.subscriptions.push(vscode.commands.registerCommand('calva.startOrConnectRepl', jackIn.startOrConnectRepl)); + context.subscriptions.push(vscode.commands.registerCommand('calva.startNewRepl', jackIn.startOrConnectRepl)); context.subscriptions.push(vscode.commands.registerCommand('calva.jackIn', jackIn.calvaJackIn)); context.subscriptions.push(vscode.commands.registerCommand('calva.copyJackInCommandToClipboard', jackIn.copyJackInCommandToClipboard)); - context.subscriptions.push(vscode.commands.registerCommand('calva.connectNonProjectREPL', connector.connectNonProjectREPLCommand)); + context.subscriptions.push(vscode.commands.registerCommand('calva.connectNonProjectREPL', () => { + connector.connectNonProjectREPLCommand(context) + })); context.subscriptions.push(vscode.commands.registerCommand('calva.connect', connector.connectCommand)); context.subscriptions.push(vscode.commands.registerCommand('calva.disconnect', jackIn.calvaDisconnect)); context.subscriptions.push(vscode.commands.registerCommand('calva.toggleCLJCSession', connector.toggleCLJCSession)); diff --git a/src/nrepl/connectSequence.ts b/src/nrepl/connectSequence.ts index b3e6d76be..ac66276c7 100644 --- a/src/nrepl/connectSequence.ts +++ b/src/nrepl/connectSequence.ts @@ -205,7 +205,7 @@ function getCustomConnectSequences(): ReplConnectSequence[] { */ function getConnectSequences(projectTypes: string[]): ReplConnectSequence[] { const customSequences = getCustomConnectSequences(); - const defSequences = projectTypes.reduce((seqs, projecType) => seqs.concat(defaultSequences[projecType]), []); + const defSequences = projectTypes.reduce((seqs, projectType) => seqs.concat(defaultSequences[projectType]), []); const defSequenceProjectTypes = [...new Set(defSequences.map(s => s.projectType))]; const sequences = customSequences.filter(customSequence => defSequenceProjectTypes.includes(customSequence.projectType)).concat(defSequences); return sequences; diff --git a/src/nrepl/jack-in.ts b/src/nrepl/jack-in.ts index cf3d1732b..c98f8fe12 100644 --- a/src/nrepl/jack-in.ts +++ b/src/nrepl/jack-in.ts @@ -247,7 +247,7 @@ export async function calvaDisconnect() { vscode.window.showInformationMessage("Not connected to a REPL server"); } -export async function calvaJackInOrConnect() { +export async function startOrConnectRepl() { const JACK_IN_OPTION = "Start a REPL server and connect (a.k.a. Jack-in)"; const JACK_IN_COMMAND = "calva.jackIn"; let commands = {}; diff --git a/src/nrepl/project-types.ts b/src/nrepl/project-types.ts index ddc679f52..db8210bb7 100644 --- a/src/nrepl/project-types.ts +++ b/src/nrepl/project-types.ts @@ -7,6 +7,7 @@ import * as pprint from '../printer'; import { keywordize, unKeywordize } from '../util/string'; import { CljsTypes, ReplConnectSequence } from './connectSequence'; +import { pathToNs } from '../util/ns-form'; const { parseForms, parseEdn } = require('../../out/cljs-lib/cljs-lib'); export const isWin = /^win/.test(process.platform); @@ -60,7 +61,7 @@ export function nreplPortFileUri(connectSequence: ReplConnectSequence): vscode.U try { return vscode.Uri.joinPath(projectRoot, relativePath); } catch (e) { - console.log(e); + return vscode.Uri.file(path.join(projectRoot.fsPath, relativePath)); } } return vscode.Uri.file(relativePath); @@ -495,7 +496,7 @@ export async function detectProjectTypes(): Promise { } export function getAllProjectTypes(): string[] { - return [...Object.keys(projectTypes)]; + return ['generic', ...Object.keys(projectTypes).filter(pt => pt !== 'generic')]; } export function getCljsTypeName(connectSequence: ReplConnectSequence) { diff --git a/src/results-output/results-doc.ts b/src/results-output/results-doc.ts index f2e08aa82..aac554088 100644 --- a/src/results-output/results-doc.ts +++ b/src/results-output/results-doc.ts @@ -36,12 +36,14 @@ export const CLJS_CONNECT_GREETINGS = '; TIPS: You can choose which REPL to use ; *Calva: Toggle REPL connection*\n\ ; (There is a button in the status bar for this)'; + function outputFileDir() { let projectRoot = state.getProjectRootUri(); - if (!projectRoot) { - projectRoot = vscode.Uri.file(os.tmpdir()); + try { + return vscode.Uri.joinPath(projectRoot, ".calva", "output-window"); + } catch { + return vscode.Uri.file(path.join(projectRoot.fsPath, ".calva", "output-window")); } - return vscode.Uri.joinPath(projectRoot, ".calva", "output-window"); }; const DOC_URI = () => { diff --git a/src/state.ts b/src/state.ts index 9c6bd4929..172810bd5 100644 --- a/src/state.ts +++ b/src/state.ts @@ -6,6 +6,7 @@ import { ReplConnectSequence } from './nrepl/connectSequence'; import { JackInDependency } from './nrepl/project-types'; import * as util from './utilities'; import * as path from 'path'; +import * as os from 'os'; import * as fs from 'fs'; import { customREPLCommandSnippet } from './evaluate'; import { PrettyPrintingOptions } from './printer'; @@ -140,7 +141,29 @@ export function getProjectRootUri(useCache = true): vscode.Uri { } } -export function getProjectWsFolder(): vscode.WorkspaceFolder { +export async function getOrCreateNonProjectRoot(context: vscode.ExtensionContext): Promise { + const NON_PROJECT_DIR_KEY = "calva.connect.nonProjectDir"; + let root = getProjectRootUri(); + if (!root) { + try { + root = await context.workspaceState.get(NON_PROJECT_DIR_KEY) as vscode.Uri; + } catch { + root = await context.globalState.get(NON_PROJECT_DIR_KEY) as vscode.Uri; + } + } + if (!root) { + const subDir = Math.random().toString(36).substring(7); + root = vscode.Uri.file(path.join(os.tmpdir(), subDir)); + } + try { + context.workspaceState.update(NON_PROJECT_DIR_KEY, root); + } catch { + context.globalState.update(NON_PROJECT_DIR_KEY, root); + } + return root; +} + +function getProjectWsFolder(): vscode.WorkspaceFolder { const doc = util.getDocument({}); if (doc) { const folder = vscode.workspace.getWorkspaceFolder(doc.uri); @@ -156,8 +179,7 @@ export function getProjectWsFolder(): vscode.WorkspaceFolder { /** * Figures out, and stores, the current clojure project root - * Also stores the WorkSpace folder for the project to be used - * when executing the Task and get proper vscode reporting. + * Also stores the WorkSpace folder for the project. * * 1. If there is no file open in single-rooted workspace use * the workspace folder as a starting point. In multi-rooted @@ -166,63 +188,57 @@ export function getProjectWsFolder(): vscode.WorkspaceFolder { * by looking for project files from the file's directory and up to * the window root (for plain folder windows) or the file's * workspace folder root (for workspaces) to find the project root. - * - * If there is no project file found, throw an exception. */ -export async function initProjectDir(): Promise { - const projectFileNames: string[] = ["project.clj", "shadow-cljs.edn", "deps.edn"]; - const workspace = vscode.workspace.workspaceFolders![0]; - const doc = util.getDocument({}); - - // first try the workplace folder - let workspaceFolder = doc ? vscode.workspace.getWorkspaceFolder(doc.uri) : null; - if (!workspaceFolder) { - if (vscode.workspace.workspaceFolders.length == 1) { - // this is only save in a one directory workspace - // (aks "Open Folder") environment. - workspaceFolder = workspace ? vscode.workspace.getWorkspaceFolder(workspace.uri) : null; - } +export async function initProjectDir(uri?: vscode.Uri): Promise { + if (uri) { + cursor.set(PROJECT_DIR_KEY, path.resolve(uri.fsPath)); + cursor.set(PROJECT_DIR_URI_KEY, uri); + } else { + const projectFileNames: string[] = ["project.clj", "shadow-cljs.edn", "deps.edn"]; + const doc = util.getDocument({}); + let workspaceFolder = getProjectWsFolder(); + await findLocalProjectRoot(projectFileNames, doc, workspaceFolder); + await findProjectRootUri(projectFileNames, doc, workspaceFolder); } - - await findLocalProjectRoot(projectFileNames, doc, workspaceFolder); - await findProjectRootUri(projectFileNames, doc, workspaceFolder); } async function findLocalProjectRoot(projectFileNames, doc, workspaceFolder): Promise { - let rootPath: string = path.resolve(workspaceFolder.uri.fsPath); - cursor.set(PROJECT_DIR_KEY, rootPath); - cursor.set(PROJECT_DIR_URI_KEY, workspaceFolder.uri); + if (workspaceFolder) { + let rootPath: string = path.resolve(workspaceFolder.uri.fsPath); + cursor.set(PROJECT_DIR_KEY, rootPath); + cursor.set(PROJECT_DIR_URI_KEY, workspaceFolder.uri); - let d = null; - let prev = null; - if (doc && path.dirname(doc.uri.fsPath) !== '.') { - d = path.dirname(doc.uri.fsPath); - } else { - d = workspaceFolder.uri.fsPath; - } - while (d !== prev) { - for (let projectFile in projectFileNames) { - const p = path.resolve(d, projectFileNames[projectFile]); - if (fs.existsSync(p)) { - rootPath = d; + let d = null; + let prev = null; + if (doc && path.dirname(doc.uri.fsPath) !== '.') { + d = path.dirname(doc.uri.fsPath); + } else { + d = workspaceFolder.uri.fsPath; + } + while (d !== prev) { + for (let projectFile in projectFileNames) { + const p = path.resolve(d, projectFileNames[projectFile]); + if (fs.existsSync(p)) { + rootPath = d; + break; + } + } + if (d === rootPath) { break; } + prev = d; + d = path.resolve(d, ".."); } - if (d === rootPath) { - break; - } - prev = d; - d = path.resolve(d, ".."); - } - // at least be sure the the root folder contains a - // supported project. - for (let projectFile in projectFileNames) { - const p = path.resolve(rootPath, projectFileNames[projectFile]); - if (fs.existsSync(p)) { - cursor.set(PROJECT_DIR_KEY, rootPath); - cursor.set(PROJECT_DIR_URI_KEY, vscode.Uri.file(rootPath)); - return; + // at least be sure the the root folder contains a + // supported project. + for (let projectFile in projectFileNames) { + const p = path.resolve(rootPath, projectFileNames[projectFile]); + if (fs.existsSync(p)) { + cursor.set(PROJECT_DIR_KEY, rootPath); + cursor.set(PROJECT_DIR_URI_KEY, vscode.Uri.file(rootPath)); + return; + } } } return; @@ -230,24 +246,26 @@ async function findLocalProjectRoot(projectFileNames, doc, workspaceFolder): Pro async function findProjectRootUri(projectFileNames, doc, workspaceFolder): Promise { let searchUri = doc?.uri || workspaceFolder?.uri; - let prev = null; - while (searchUri != prev) { - try { - for (let projectFile in projectFileNames) { - const u = vscode.Uri.joinPath(searchUri, projectFileNames[projectFile]); - try { - await vscode.workspace.fs.stat(u); - cursor.set(PROJECT_DIR_URI_KEY, searchUri); - return; + if (searchUri && !(searchUri.scheme === 'untitled')) { + let prev = null; + while (searchUri != prev) { + try { + for (let projectFile in projectFileNames) { + const u = vscode.Uri.joinPath(searchUri, projectFileNames[projectFile]); + try { + await vscode.workspace.fs.stat(u); + cursor.set(PROJECT_DIR_URI_KEY, searchUri); + return; + } + catch { } } - catch { } } + catch (e) { + console.error(`Problems in search for project root directory: ${e}`); + } + prev = searchUri; + searchUri = vscode.Uri.joinPath(searchUri, ".."); } - catch (e) { - console.error(`Problems in search for project root directory: ${e}`); - } - prev = searchUri; - searchUri = vscode.Uri.joinPath(searchUri, ".."); } } diff --git a/src/statusbar.ts b/src/statusbar.ts index 6cf95cb20..a7310ac91 100644 --- a/src/statusbar.ts +++ b/src/statusbar.ts @@ -57,7 +57,7 @@ function update(context = state.extensionContext) { connectionStatus.text = "nREPL $(zap)"; connectionStatus.color = colorValue("connectedStatusColor", currentConf); connectionStatus.tooltip = `nrepl://${current.get('hostname')}:${current.get('port')} (Click to reset connection)`; - connectionStatus.command = "calva.jackInOrConnect"; + connectionStatus.command = "calva.startOrConnectRepl"; typeStatus.color = colorValue("typeStatusColor", currentConf); const replType = namespace.getREPLSessionType(); if (replType !== null) { @@ -91,7 +91,7 @@ function update(context = state.extensionContext) { connectionStatus.text = "nREPL $(zap)"; connectionStatus.tooltip = "Click to jack-in or Connect to REPL Server"; connectionStatus.color = colorValue("disconnectedColor", currentConf); - connectionStatus.command = "calva.jackInOrConnect"; + connectionStatus.command = "calva.startOrConnectRepl"; } if (status.shouldshowReplUi(context)) { connectionStatus.show();