Skip to content

Commit

Permalink
Refactor support for multiple Meson projects
Browse files Browse the repository at this point in the history
The very first thing the extension must do is determining the Meson
project source directory. If there are multiple workspaces, or if the
repository contains multiple projects in subdirs, there could be more
than one potential Meson project. In that case ask the user to select
one. The build directory can then be determined relative to the selected
source directory.

This also adds a command to allow switching to a different Meson project
root directory. Unfortunately this requires a full window reload for
now.
  • Loading branch information
xclaesse committed Nov 27, 2023
1 parent 7cd380c commit e79163e
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 78 deletions.
13 changes: 13 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,20 @@
{
"command": "mesonbuild.restartLanguageServer",
"title": "Meson: Restart Language Server"
},
{
"command": "mesonbuild.selectRootDir",
"title": "Meson: Select project root directory"
}
],
"configuration": {
"title": "Meson build configuration",
"properties": {
"mesonbuild.selectRootDir": {
"type": "boolean",
"default": true,
"description": "Ask to select a Meson project root directory when more than one project is detected."
},
"mesonbuild.configureOnOpen": {
"type": [
"boolean",
Expand Down Expand Up @@ -437,6 +446,10 @@
{
"command": "mesonbuild.restartLanguageServer",
"when": "mesonbuild.hasProject"
},
{
"command": "mesonbuild.selectRootDir",
"when": "mesonbuild.hasMultipleProjects"
}
]
},
Expand Down
49 changes: 49 additions & 0 deletions src/dialogs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as vscode from "vscode";
import { relative } from "path";
import { extensionConfiguration, extensionConfigurationSet } from "./utils";
import { SettingsKey } from "./types";

Expand Down Expand Up @@ -64,3 +65,51 @@ export async function askShouldDownload(): Promise<boolean> {

return false;
}

export async function askSelectRootDir(): Promise<boolean> {
const selectRootDir = extensionConfiguration(SettingsKey.selectRootDir);

if (!selectRootDir) return false;

enum Options {
yes = "Yes",
no = "No",
never = "Never",
}

const response = await vscode.window.showInformationMessage(
"Multiple Meson projects detected, select one?",
...Object.values(Options),
);

switch (response) {
case Options.yes:
return true;
case Options.no:
return false;
case Options.never:
extensionConfigurationSet(SettingsKey.selectRootDir, false, vscode.ConfigurationTarget.Workspace);
return false;
}

return false;
}

export async function selectRootDir(rootDirs: string[]): Promise<string | undefined> {
// TODO: What label to use when there is more than one workspace?
const root = vscode.workspace.rootPath ?? "";
const items = rootDirs.map((file, index) => ({ index: index, label: relative(root, file) }));
items.sort((a, b) => a.label.localeCompare(b.label));
const selection = await vscode.window.showQuickPick(items, {
canPickMany: false,
title: "Select configuration to use.",
placeHolder: "path/to/meson.build",
});
if (selection) return rootDirs[selection.index];
return undefined;
}

export async function askAndSelectRootDir(rootDirs: string[]): Promise<string | undefined> {
if (await askSelectRootDir()) return selectRootDir(rootDirs);
return undefined;
}
98 changes: 36 additions & 62 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
checkMesonIsConfigured,
getOutputChannel,
getBuildDirectory,
rootMesonFiles,
mesonRootDirs,
} from "./utils";
import { DebugConfigurationProviderCppdbg } from "./debug/cppdbg";
import { DebugConfigurationProviderLldb } from "./debug/lldb";
Expand All @@ -19,52 +19,58 @@ import { activateLinters } from "./linters";
import { activateFormatters } from "./formatters";
import { SettingsKey, TaskQuickPickItem } from "./types";
import { createLanguageServerClient } from "./lsp/common";
import { dirname, relative } from "path";
import { askShouldDownload, askConfigureOnOpen } from "./dialogs";
import { askShouldDownload, askConfigureOnOpen, askAndSelectRootDir, selectRootDir } from "./dialogs";

export let extensionPath: string;
export let workspaceState: vscode.Memento;
let explorer: MesonProjectExplorer;
let watcher: vscode.FileSystemWatcher;
let compileCommandsWatcher: vscode.FileSystemWatcher;
let mesonWatcher: vscode.FileSystemWatcher;
let controller: vscode.TestController;

export async function activate(ctx: vscode.ExtensionContext) {
extensionPath = ctx.extensionPath;
workspaceState = ctx.workspaceState;

if (!vscode.workspace.workspaceFolders) {
return;
}

const root = vscode.workspace.workspaceFolders[0].uri.fsPath;
const mesonFiles = await rootMesonFiles();
if (mesonFiles.length === 0) {
return;
}

let configurationChosen = false;
let savedMesonFile = workspaceState.get<string>("mesonbuild.mesonFile");
if (savedMesonFile) {
const filePaths = mesonFiles.map((file) => file.fsPath);
if (filePaths.includes(savedMesonFile)) {
configurationChosen = workspaceState.get<boolean>("mesonbuild.configurationChosen") ?? false;
// The workspace could contain multiple Meson projects. Take all root
// meson.build files we find. Usually that's just one at the root of the
// workspace.
const rootDirs = await mesonRootDirs();
let rootDir: string | undefined = undefined;
if (rootDirs.length == 1) {
rootDir = rootDirs[0];
} else if (rootDirs.length > 1) {
let savedSourceDir = workspaceState.get<string>("mesonbuild.sourceDir");
if (savedSourceDir && rootDirs.includes(savedSourceDir)) {
rootDir = savedSourceDir;
} else {
savedMesonFile = undefined;
// We have more than one root meson.build file and none has been previously
// saved. Ask the user to pick one.
rootDir = await askAndSelectRootDir(rootDirs);
}
}

const mesonFile = savedMesonFile ?? mesonFiles[0].fsPath;
const sourceDir = dirname(mesonFile);
const buildDir = getBuildDirectory(sourceDir);
ctx.subscriptions.push(
vscode.commands.registerCommand("mesonbuild.selectRootDir", async () => {
let newRootDir = await selectRootDir(rootDirs);
if (newRootDir && newRootDir != rootDir) {
await workspaceState.update("mesonbuild.sourceDir", newRootDir);
vscode.commands.executeCommand("workbench.action.reloadWindow");
}
}),
);

workspaceState.update("mesonbuild.mesonFile", mesonFile);
getOutputChannel().appendLine(`Meson project root: ${rootDir}`);
vscode.commands.executeCommand("setContext", "mesonbuild.hasProject", rootDir !== undefined);
vscode.commands.executeCommand("setContext", "mesonbuild.hasMultipleProjects", rootDirs.length > 1);
if (!rootDir) return;

const sourceDir = rootDir;
const buildDir = getBuildDirectory(sourceDir);
workspaceState.update("mesonbuild.buildDir", buildDir);
workspaceState.update("mesonbuild.sourceDir", sourceDir);
workspaceState.update("mesonbuild.configurationChosen", undefined);

explorer = new MesonProjectExplorer(ctx, root, buildDir);
explorer = new MesonProjectExplorer(ctx, sourceDir, buildDir);

const providers = [DebugConfigurationProviderCppdbg, DebugConfigurationProviderLldb];
providers.forEach((provider) => {
Expand All @@ -74,16 +80,6 @@ export async function activate(ctx: vscode.ExtensionContext) {
);
});

const updateHasProject = async () => {
const mesonFiles = await vscode.workspace.findFiles("**/meson.build");
vscode.commands.executeCommand("setContext", "mesonbuild.hasProject", mesonFiles.length > 0);
};
mesonWatcher = vscode.workspace.createFileSystemWatcher("**/meson.build", false, true, false);
mesonWatcher.onDidCreate(updateHasProject);
mesonWatcher.onDidDelete(updateHasProject);
ctx.subscriptions.push(mesonWatcher);
await updateHasProject();

controller = vscode.tests.createTestController("meson-test-controller", "Meson test controller");
controller.createRunProfile(
"Meson debug test",
Expand Down Expand Up @@ -202,29 +198,7 @@ export async function activate(ctx: vscode.ExtensionContext) {
);

if (!checkMesonIsConfigured(buildDir)) {
let configureOnOpen = await askConfigureOnOpen();

if (configureOnOpen) {
let cancel = false;
if (!configurationChosen && mesonFiles.length > 1) {
const items = mesonFiles.map((file, index) => ({ index: index, label: relative(root, file.fsPath) }));
items.sort((a, b) => a.label.localeCompare(b.label));
const selection = await vscode.window.showQuickPick(items, {
canPickMany: false,
title: "Select configuration to use.",
placeHolder: "path/to/meson.build",
});
if (selection && mesonFiles[selection.index].fsPath !== mesonFile) {
await workspaceState.update("mesonbuild.mesonFile", mesonFiles[selection.index].fsPath);
await workspaceState.update("mesonbuild.configurationChosen", true);
vscode.commands.executeCommand("workbench.action.reloadWindow");
}
cancel = selection === undefined;
}
if (!cancel) {
runFirstTask("reconfigure");
}
}
if (await askConfigureOnOpen()) runFirstTask("reconfigure");
} else {
await rebuildTests(controller);
}
Expand All @@ -247,8 +221,8 @@ export async function activate(ctx: vscode.ExtensionContext) {

getOutputChannel().appendLine("Not enabling the muon linter/formatter because Swift-MesonLSP is active.");
} else {
activateLinters(root, ctx);
activateFormatters(root, ctx);
activateLinters(sourceDir, ctx);
activateFormatters(sourceDir, ctx);
}

ctx.subscriptions.push(
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface ExtensionConfiguration {
languageServer: LanguageServer;
languageServerPath: string;
downloadLanguageServer: boolean | "ask";
selectRootDir: boolean;
}

export interface TaskQuickPickItem extends vscode.QuickPickItem {
Expand Down Expand Up @@ -129,4 +130,5 @@ export enum SettingsKey {
downloadLanguageServer = "downloadLanguageServer",
languageServer = "languageServer",
configureOnOpen = "configureOnOpen",
selectRootDir = "selectRootDir",
}
36 changes: 20 additions & 16 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ export function extensionRelative(filepath: string) {
export function getBuildDirectory(sourceDir: string) {
const buildDir = extensionConfiguration("buildFolder");
if (path.isAbsolute(buildDir)) return buildDir;

return path.join(sourceDir, buildDir);
}

Expand Down Expand Up @@ -182,23 +181,28 @@ export function checkMesonIsConfigured(buildDir: string) {
return fs.existsSync(path.join(buildDir, "meson-private", "coredata.dat"));
}

export async function rootMesonFiles(): Promise<vscode.Uri[]> {
const allFiles = (await vscode.workspace.findFiles("**/meson.build")).sort(
(a, b) => a.fsPath.length - b.fsPath.length,
);

const rootFiles: vscode.Uri[] = [];
for (const a of allFiles) {
if (rootFiles.length === 0) {
rootFiles.push(a);
continue;
export async function mesonRootDirs(): Promise<string[]> {
let rootDirs: string[] = [];
let pending: vscode.Uri[] = [];
vscode.workspace.workspaceFolders!.forEach((i) => pending.push(i.uri));
while (true) {
const d = pending.pop();
if (!d) break;
let hasMesonFile: boolean = false;
let subdirs: vscode.Uri[] = [];
for (const [name, type] of await vscode.workspace.fs.readDirectory(d)) {
if (type === vscode.FileType.File && name == "meson.build") {
rootDirs.push(d.fsPath);
hasMesonFile = true;
break;
} else if (type === vscode.FileType.Directory) {
subdirs.push(vscode.Uri.joinPath(d, name));
}
}

if (!path.dirname(a.fsPath).startsWith(path.dirname(rootFiles[rootFiles.length - 1].fsPath))) {
rootFiles.push(a);
continue;
if (!hasMesonFile) {
pending.push(...subdirs);
}
}

return rootFiles;
return rootDirs;
}

0 comments on commit e79163e

Please sign in to comment.