diff --git a/client/src/extension.ts b/client/src/extension.ts index 666445c4..65c5ce91 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -153,6 +153,10 @@ async function handleError({ message, actions }: { message: string; actions: Act } export async function activate(context: vscode.ExtensionContext, launchOptions: LaunchOptions = defaultLaunchOptions) { + if (!isProjectMode()) { + vscode.window.showWarningMessage(USER_MESSAGE.SINGLE_FILE_MODE()) + } + // activate state initialiseState(context) @@ -205,50 +209,64 @@ export async function activate(context: vscode.ExtensionContext, launchOptions: // While most other commands can be awaited directly, this is useful for stuff like file creation, which indirectely triggers an asynchronous job. registerCommand('flix.allJobsFinished', handlers.allJobsFinished(client, eventEmitter)) - // watch for changes on the file system (delete, create, rename .flix files) - flixWatcher = vscode.workspace.createFileSystemWatcher(getFlixGlobPattern()) - flixWatcher.onDidDelete((vsCodeUri: vscode.Uri) => { - const uri = vsCodeUriToUriString(vsCodeUri) - client.sendNotification(jobs.Request.apiRemUri, { uri }) - }) - flixWatcher.onDidCreate((vsCodeUri: vscode.Uri) => { - const uri = vsCodeUriToUriString(vsCodeUri) - client.sendNotification(jobs.Request.apiAddUri, { uri }) - }) + if (isProjectMode()) { + // In project mode, watch the file system for .flix/.fpkg/.jar/flix.toml changes. - // watch for changes on the file system (delete, create .fpkg files) - pkgWatcher = vscode.workspace.createFileSystemWatcher(getFpkgGlobPattern()) - pkgWatcher.onDidDelete((vsCodeUri: vscode.Uri) => { - const uri = vsCodeUriToUriString(vsCodeUri) - client.sendNotification(jobs.Request.apiRemPkg, { uri }) - }) - pkgWatcher.onDidCreate((vsCodeUri: vscode.Uri) => { - const uri = vsCodeUriToUriString(vsCodeUri) - client.sendNotification(jobs.Request.apiAddPkg, { uri }) - }) + flixWatcher = vscode.workspace.createFileSystemWatcher(getFlixGlobPattern()) + flixWatcher.onDidDelete((vsCodeUri: vscode.Uri) => { + const uri = vsCodeUriToUriString(vsCodeUri) + client.sendNotification(jobs.Request.apiRemUri, { uri }) + }) + flixWatcher.onDidCreate((vsCodeUri: vscode.Uri) => { + const uri = vsCodeUriToUriString(vsCodeUri) + client.sendNotification(jobs.Request.apiAddUri, { uri }) + }) - // watch for changes on the file system (delete, create .jar files) - pkgWatcher = vscode.workspace.createFileSystemWatcher(getJarGlobPattern()) - pkgWatcher.onDidDelete((vsCodeUri: vscode.Uri) => { - const uri = vsCodeUriToUriString(vsCodeUri) - client.sendNotification(jobs.Request.apiRemJar, { uri }) - }) - pkgWatcher.onDidCreate((vsCodeUri: vscode.Uri) => { - const uri = vsCodeUriToUriString(vsCodeUri) - client.sendNotification(jobs.Request.apiAddJar, { uri }) - }) + pkgWatcher = vscode.workspace.createFileSystemWatcher(getFpkgGlobPattern()) + pkgWatcher.onDidDelete((vsCodeUri: vscode.Uri) => { + const uri = vsCodeUriToUriString(vsCodeUri) + client.sendNotification(jobs.Request.apiRemPkg, { uri }) + }) + pkgWatcher.onDidCreate((vsCodeUri: vscode.Uri) => { + const uri = vsCodeUriToUriString(vsCodeUri) + client.sendNotification(jobs.Request.apiAddPkg, { uri }) + }) - // watch for changes to the flix.toml file - tomlWatcher = vscode.workspace.createFileSystemWatcher(getFlixTomlGlobPattern()) - tomlWatcher.onDidChange(() => { - const { msg, option1, option2 } = USER_MESSAGE.ASK_RELOAD_TOML() - const doReload = vscode.window.showInformationMessage(msg, option1, option2) - doReload.then(res => { - if (res === 'Yes') { - makeHandleRestartClient(context, launchOptions)() + pkgWatcher = vscode.workspace.createFileSystemWatcher(getJarGlobPattern()) + pkgWatcher.onDidDelete((vsCodeUri: vscode.Uri) => { + const uri = vsCodeUriToUriString(vsCodeUri) + client.sendNotification(jobs.Request.apiRemJar, { uri }) + }) + pkgWatcher.onDidCreate((vsCodeUri: vscode.Uri) => { + const uri = vsCodeUriToUriString(vsCodeUri) + client.sendNotification(jobs.Request.apiAddJar, { uri }) + }) + + tomlWatcher = vscode.workspace.createFileSystemWatcher(getFlixTomlGlobPattern()) + tomlWatcher.onDidChange(() => { + const { msg, option1, option2 } = USER_MESSAGE.ASK_RELOAD_TOML() + const doReload = vscode.window.showInformationMessage(msg, option1, option2) + doReload.then(res => { + if (res === 'Yes') { + makeHandleRestartClient(context, launchOptions)() + } + }) + }) + } else { + // In single-file mode there is no workspace folder to watch. + // Instead, track document open/close to add/remove .flix files from the compiler. + // Content changes are already handled by the LSP TextDocumentSync mechanism. + vscode.workspace.onDidOpenTextDocument(doc => { + if (doc.uri.path.endsWith('.flix')) { + client.sendNotification(jobs.Request.apiAddUri, { uri: vsCodeUriToUriString(doc.uri) }) } }) - }) + vscode.workspace.onDidCloseTextDocument(doc => { + if (doc.uri.path.endsWith('.flix')) { + client.sendNotification(jobs.Request.apiRemUri, { uri: vsCodeUriToUriString(doc.uri) }) + } + }) + } vscode.window.onDidChangeActiveTextEditor(handlers.handleChangeEditor) vscode.workspace.onDidChangeConfiguration(() => { @@ -274,9 +292,23 @@ async function startSession( const globalStoragePath = context.globalStorageUri.fsPath const workspaceFolders = vscode.workspace.workspaceFolders?.map(ws => ws.uri.fsPath) - const workspaceFiles = (await vscode.workspace.findFiles(getFlixGlobPattern())).map(vsCodeUriToUriString) - const workspacePkgs = (await vscode.workspace.findFiles(getFpkgGlobPattern())).map(vsCodeUriToUriString) - const workspaceJars = (await vscode.workspace.findFiles(getJarGlobPattern())).map(vsCodeUriToUriString) + + // In project mode, discover files via workspace glob patterns. + // In single-file mode, use the currently open .flix documents instead. + let workspaceFiles: string[] + let workspacePkgs: string[] + let workspaceJars: string[] + if (isProjectMode()) { + workspaceFiles = (await vscode.workspace.findFiles(getFlixGlobPattern())).map(vsCodeUriToUriString) + workspacePkgs = (await vscode.workspace.findFiles(getFpkgGlobPattern())).map(vsCodeUriToUriString) + workspaceJars = (await vscode.workspace.findFiles(getJarGlobPattern())).map(vsCodeUriToUriString) + } else { + workspaceFiles = vscode.workspace.textDocuments + .filter(doc => doc.uri.path.endsWith('.flix')) + .map(doc => vsCodeUriToUriString(doc.uri)) + workspacePkgs = [] + workspaceJars = [] + } // Wait until we're sure flix exists const flixFilename = await ensureFlixExists({ diff --git a/client/src/handlers/handlers.ts b/client/src/handlers/handlers.ts index 28a6cfbb..2fdc534d 100644 --- a/client/src/handlers/handlers.ts +++ b/client/src/handlers/handlers.ts @@ -1,10 +1,11 @@ +import * as path from 'path' import * as vscode from 'vscode' import { LanguageClient } from 'vscode-languageclient/node' import { EventEmitter } from 'events' import * as jobs from '../engine/jobs' import ensureFlixExists from './../util/ensureFlixExists' -import { LaunchOptions, defaultLaunchOptions, getFlixGlobPattern } from './../extension' +import { LaunchOptions, defaultLaunchOptions, getFlixGlobPattern, isProjectMode } from './../extension' import { USER_MESSAGE } from '../util/userMessages' let flixTerminal: vscode.Terminal | null = null @@ -57,10 +58,17 @@ async function launchRepl(context: vscode.ExtensionContext, launchOptions: Launc const { cmd, args } = await getJvmCmd(context, launchOptions) args.push('repl') args.push(...getExtraFlixArgs()) + + // In single-file mode, set cwd to the active file's directory so the REPL + // can find the file. In project mode, VS Code defaults to the workspace root. + const activeFilePath = vscode.window.activeTextEditor?.document.uri.fsPath + const cwd = !isProjectMode() && activeFilePath ? path.dirname(activeFilePath) : undefined + flixTerminal = vscode.window.createTerminal({ name: 'Flix REPL', shellPath: cmd, shellArgs: args, + cwd, // The terminal will not be kept alive when restarting VSCode. // This is necessary in the case where flix.jar has been removed while VSCode has been closed. @@ -155,6 +163,11 @@ export function handleChangeEditor(editor: vscode.TextEditor | undefined) { return } + // In single-file mode every .flix file is valid — there is no project boundary. + if (!isProjectMode()) { + return + } + const included = vscode.languages.match({ pattern: getFlixGlobPattern() }, editor.document) if (!included) { vscode.window.showWarningMessage(USER_MESSAGE.FILE_NOT_PART_OF_PROJECT()) diff --git a/client/src/util/userMessages.ts b/client/src/util/userMessages.ts index 0cc3fe65..4db01cd4 100644 --- a/client/src/util/userMessages.ts +++ b/client/src/util/userMessages.ts @@ -86,4 +86,8 @@ export class USER_MESSAGE { static FILE_NOT_PART_OF_PROJECT() { return `Flix will only load source files from \`*.flix\`, \`src/**\`, and \`test/**\`` } + + static SINGLE_FILE_MODE() { + return 'Running in single-file mode. Open a folder for full project support.' + } }