Auto merge of #11557 - bruno-ortiz:rust-dependencies, r=bruno-ortiz

Creating rust dependencies tree explorer

Hello!

I tried to implement a tree view that shows the dependencies of a project.

It allows to see all dependencies to the project and it uses `cargo tree` for it. Also it allows to click and open the files, the viewtree tries its best to follow the openned file in the editor.

Here is an example:
![image](https://user-images.githubusercontent.com/5748995/155822183-1e227c7b-7929-4fc8-8eed-29ccfc5e14fe.png)

Any feedback is welcome since i have basically no professional experience with TS.
This commit is contained in:
bors 2023-05-02 14:49:38 +00:00
commit a48e0e14e1
17 changed files with 522 additions and 13 deletions

View file

@ -0,0 +1,37 @@
use ide_db::{
base_db::{CrateOrigin, FileId, SourceDatabase},
FxIndexSet, RootDatabase,
};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CrateInfo {
pub name: Option<String>,
pub version: Option<String>,
pub root_file_id: FileId,
}
// Feature: Show Dependency Tree
//
// Shows a view tree with all the dependencies of this project
//
// |===
// image::https://user-images.githubusercontent.com/5748995/229394139-2625beab-f4c9-484b-84ed-ad5dee0b1e1a.png[]
pub(crate) fn fetch_crates(db: &RootDatabase) -> FxIndexSet<CrateInfo> {
let crate_graph = db.crate_graph();
crate_graph
.iter()
.map(|crate_id| &crate_graph[crate_id])
.filter(|&data| !matches!(data.origin, CrateOrigin::Local { .. }))
.map(|data| crate_info(data))
.collect()
}
fn crate_info(data: &ide_db::base_db::CrateData) -> CrateInfo {
let crate_name = crate_name(data);
let version = data.version.clone();
CrateInfo { name: crate_name, version, root_file_id: data.root_file_id }
}
fn crate_name(data: &ide_db::base_db::CrateData) -> Option<String> {
data.display_name.as_ref().map(|it| it.canonical_name().to_owned())
}

View file

@ -59,16 +59,18 @@ mod view_mir;
mod interpret_function;
mod view_item_tree;
mod shuffle_crate_graph;
mod fetch_crates;
use std::sync::Arc;
use cfg::CfgOptions;
use fetch_crates::CrateInfo;
use ide_db::{
base_db::{
salsa::{self, ParallelDatabase},
CrateOrigin, Env, FileLoader, FileSet, SourceDatabase, VfsPath,
},
symbol_index, FxHashMap, LineIndexDatabase,
symbol_index, FxHashMap, FxIndexSet, LineIndexDatabase,
};
use syntax::SourceFile;
@ -331,6 +333,10 @@ impl Analysis {
self.with_db(|db| view_crate_graph::view_crate_graph(db, full))
}
pub fn fetch_crates(&self) -> Cancellable<FxIndexSet<CrateInfo>> {
self.with_db(|db| fetch_crates::fetch_crates(db))
}
pub fn expand_macro(&self, position: FilePosition) -> Cancellable<Option<ExpandedMacro>> {
self.with_db(|db| expand_macro::expand_macro(db, position))
}

View file

@ -184,6 +184,13 @@ impl AbsPath {
self.0.ends_with(&suffix.0)
}
pub fn name_and_extension(&self) -> Option<(&str, Option<&str>)> {
Some((
self.file_stem()?.to_str()?,
self.extension().and_then(|extension| extension.to_str()),
))
}
// region:delegate-methods
// Note that we deliberately don't implement `Deref<Target = Path>` here.

View file

@ -102,6 +102,18 @@ fn replace_root(s: &mut String, direction: bool) {
}
}
fn replace_fake_sys_root(s: &mut String) {
let fake_sysroot_path = get_test_path("fake-sysroot");
let fake_sysroot_path = if cfg!(windows) {
let normalized_path =
fake_sysroot_path.to_str().expect("expected str").replace(r#"\"#, r#"\\"#);
format!(r#"{}\\"#, normalized_path)
} else {
format!("{}/", fake_sysroot_path.to_str().expect("expected str"))
};
*s = s.replace(&fake_sysroot_path, "$FAKESYSROOT$")
}
fn get_test_path(file: &str) -> PathBuf {
let base = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
base.join("test_data").join(file)
@ -140,6 +152,7 @@ fn to_crate_graph(project_workspace: ProjectWorkspace) -> (CrateGraph, ProcMacro
fn check_crate_graph(crate_graph: CrateGraph, expect: ExpectFile) {
let mut crate_graph = format!("{crate_graph:#?}");
replace_root(&mut crate_graph, false);
replace_fake_sys_root(&mut crate_graph);
expect.assert_eq(&crate_graph);
}

View file

@ -3,7 +3,8 @@
//! `ide` crate.
use ide::AssistResolveStrategy;
use lsp_types::{Diagnostic, DiagnosticTag, NumberOrString};
use lsp_types::{Diagnostic, DiagnosticTag, NumberOrString, Url};
use vfs::FileId;
use crate::{global_state::GlobalStateSnapshot, to_proto, Result};
@ -27,7 +28,7 @@ pub(crate) fn publish_diagnostics(
severity: Some(to_proto::diagnostic_severity(d.severity)),
code: Some(NumberOrString::String(d.code.as_str().to_string())),
code_description: Some(lsp_types::CodeDescription {
href: lsp_types::Url::parse(&format!(
href: Url::parse(&format!(
"https://rust-analyzer.github.io/manual.html#{}",
d.code.as_str()
))

View file

@ -2,6 +2,7 @@
//! Protocol. This module specifically handles requests.
use std::{
fs,
io::Write as _,
process::{self, Stdio},
sync::Arc,
@ -29,7 +30,7 @@ use project_model::{ManifestPath, ProjectWorkspace, TargetKind};
use serde_json::json;
use stdx::{format_to, never};
use syntax::{algo, ast, AstNode, TextRange, TextSize};
use vfs::{AbsPath, AbsPathBuf};
use vfs::{AbsPath, AbsPathBuf, VfsPath};
use crate::{
cargo_target_spec::CargoTargetSpec,
@ -38,7 +39,10 @@ use crate::{
from_proto,
global_state::{GlobalState, GlobalStateSnapshot},
line_index::LineEndings,
lsp_ext::{self, PositionOrRange, ViewCrateGraphParams, WorkspaceSymbolParams},
lsp_ext::{
self, CrateInfoResult, FetchDependencyListParams, FetchDependencyListResult,
PositionOrRange, ViewCrateGraphParams, WorkspaceSymbolParams,
},
lsp_utils::{all_edits_are_disjoint, invalid_params_error},
to_proto, LspError, Result,
};
@ -1881,3 +1885,52 @@ fn run_rustfmt(
Ok(Some(to_proto::text_edit_vec(&line_index, diff(&file, &new_text))))
}
}
pub(crate) fn fetch_dependency_list(
state: GlobalStateSnapshot,
_params: FetchDependencyListParams,
) -> Result<FetchDependencyListResult> {
let crates = state.analysis.fetch_crates()?;
let crate_infos = crates
.into_iter()
.filter_map(|it| {
let root_file_path = state.file_id_to_file_path(it.root_file_id);
crate_path(root_file_path).and_then(to_url).map(|path| CrateInfoResult {
name: it.name,
version: it.version,
path,
})
})
.collect();
Ok(FetchDependencyListResult { crates: crate_infos })
}
/// Searches for the directory of a Rust crate given this crate's root file path.
///
/// # Arguments
///
/// * `root_file_path`: The path to the root file of the crate.
///
/// # Returns
///
/// An `Option` value representing the path to the directory of the crate with the given
/// name, if such a crate is found. If no crate with the given name is found, this function
/// returns `None`.
fn crate_path(root_file_path: VfsPath) -> Option<VfsPath> {
let mut current_dir = root_file_path.parent();
while let Some(path) = current_dir {
let cargo_toml_path = path.join("../Cargo.toml")?;
if fs::metadata(cargo_toml_path.as_path()?).is_ok() {
let crate_path = cargo_toml_path.parent()?;
return Some(crate_path);
}
current_dir = path.parent();
}
None
}
fn to_url(path: VfsPath) -> Option<Url> {
let path = path.as_path()?;
let str_path = path.as_os_str().to_str()?;
Url::from_file_path(str_path).ok()
}

View file

@ -4,11 +4,11 @@ use std::{collections::HashMap, path::PathBuf};
use ide_db::line_index::WideEncoding;
use lsp_types::request::Request;
use lsp_types::PositionEncodingKind;
use lsp_types::{
notification::Notification, CodeActionKind, DocumentOnTypeFormattingParams,
PartialResultParams, Position, Range, TextDocumentIdentifier, WorkDoneProgressParams,
};
use lsp_types::{PositionEncodingKind, Url};
use serde::{Deserialize, Serialize};
use crate::line_index::PositionEncoding;
@ -27,6 +27,31 @@ pub struct AnalyzerStatusParams {
pub text_document: Option<TextDocumentIdentifier>,
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CrateInfoResult {
pub name: Option<String>,
pub version: Option<String>,
pub path: Url,
}
pub enum FetchDependencyList {}
impl Request for FetchDependencyList {
type Params = FetchDependencyListParams;
type Result = FetchDependencyListResult;
const METHOD: &'static str = "rust-analyzer/fetchDependencyList";
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct FetchDependencyListParams {}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct FetchDependencyListResult {
pub crates: Vec<CrateInfoResult>,
}
pub enum MemoryUsage {}
impl Request for MemoryUsage {
@ -359,6 +384,7 @@ impl Request for CodeActionRequest {
}
pub enum CodeActionResolveRequest {}
impl Request for CodeActionResolveRequest {
type Params = CodeAction;
type Result = CodeAction;

View file

@ -660,6 +660,7 @@ impl GlobalState {
.on_sync::<lsp_ext::OnEnter>(handlers::handle_on_enter)
.on_sync::<lsp_types::request::SelectionRangeRequest>(handlers::handle_selection_range)
.on_sync::<lsp_ext::MatchingBrace>(handlers::handle_matching_brace)
.on::<lsp_ext::FetchDependencyList>(handlers::fetch_dependency_list)
.on::<lsp_ext::AnalyzerStatus>(handlers::handle_analyzer_status)
.on::<lsp_ext::SyntaxTree>(handlers::handle_syntax_tree)
.on::<lsp_ext::ViewHir>(handlers::handle_view_hir)

View file

@ -107,10 +107,7 @@ impl VfsPath {
/// Returns `self`'s base name and file extension.
pub fn name_and_extension(&self) -> Option<(&str, Option<&str>)> {
match &self.0 {
VfsPathRepr::PathBuf(p) => Some((
p.file_stem()?.to_str()?,
p.extension().and_then(|extension| extension.to_str()),
)),
VfsPathRepr::PathBuf(p) => p.name_and_extension(),
VfsPathRepr::VirtualPath(p) => p.name_and_extension(),
}
}

View file

@ -1,5 +1,5 @@
<!---
lsp_ext.rs hash: 31ca513a249753ab
lsp_ext.rs hash: fdf1afd34548abbc
If you need to change the above hash to make the test pass, please check if you
need to adjust this doc as well and ping this issue:
@ -851,3 +851,26 @@ export interface Diagnostic {
rendered?: string;
};
}
```
## Dependency Tree
**Method:** `rust-analyzer/fetchDependencyList`
**Request:**
```typescript
export interface FetchDependencyListParams {}
```
**Response:**
```typescript
export interface FetchDependencyListResult {
crates: {
name: string;
version: string;
path: string;
}[];
}
```
Returns all crates from this workspace, so it can be used create a viewTree to help navigate the dependency tree.

View file

@ -284,6 +284,14 @@
"command": "rust-analyzer.clearFlycheck",
"title": "Clear flycheck diagnostics",
"category": "rust-analyzer"
},
{
"command": "rust-analyzer.revealDependency",
"title": "Reveal File"
},
{
"command": "rust-analyzer.revealDependency",
"title": "Reveal File"
}
],
"keybindings": [
@ -1956,6 +1964,14 @@
}
]
},
"views": {
"explorer": [
{
"id": "rustDependencies",
"name": "Rust Dependencies"
}
]
},
"jsonValidation": [
{
"fileMatch": "rust-project.json",

View file

@ -8,10 +8,18 @@ import { applySnippetWorkspaceEdit, applySnippetTextEdits } from "./snippets";
import { spawnSync } from "child_process";
import { RunnableQuickPick, selectRunnable, createTask, createArgs } from "./run";
import { AstInspector } from "./ast_inspector";
import { isRustDocument, isCargoTomlDocument, sleep, isRustEditor } from "./util";
import {
isRustDocument,
isCargoTomlDocument,
sleep,
isRustEditor,
RustEditor,
RustDocument,
} from "./util";
import { startDebugSession, makeDebugConfig } from "./debug";
import { LanguageClient } from "vscode-languageclient/node";
import { LINKED_COMMANDS } from "./client";
import { DependencyId } from "./dependencies_provider";
export * from "./ast_inspector";
export * from "./run";
@ -266,6 +274,71 @@ export function openCargoToml(ctx: CtxInit): Cmd {
};
}
export function revealDependency(ctx: CtxInit): Cmd {
return async (editor: RustEditor) => {
if (!ctx.dependencies?.isInitialized()) {
return;
}
const documentPath = editor.document.uri.fsPath;
const dep = ctx.dependencies?.getDependency(documentPath);
if (dep) {
await ctx.treeView?.reveal(dep, { select: true, expand: true });
} else {
await revealParentChain(editor.document, ctx);
}
};
}
/**
* This function calculates the parent chain of a given file until it reaches it crate root contained in ctx.dependencies.
* This is need because the TreeView is Lazy, so at first it only has the root dependencies: For example if we have the following crates:
* - core
* - alloc
* - std
*
* if I want to reveal alloc/src/str.rs, I have to:
* 1. reveal every children of alloc
* - core
* - alloc\
* &emsp;|-beches\
* &emsp;|-src\
* &emsp;|- ...
* - std
* 2. reveal every children of src:
* core
* alloc\
* &emsp;|-beches\
* &emsp;|-src\
* &emsp;&emsp;|- lib.rs\
* &emsp;&emsp;|- str.rs <------- FOUND IT!\
* &emsp;&emsp;|- ...\
* &emsp;|- ...\
* std
*/
async function revealParentChain(document: RustDocument, ctx: CtxInit) {
let documentPath = document.uri.fsPath;
const maxDepth = documentPath.split(path.sep).length - 1;
const parentChain: DependencyId[] = [{ id: documentPath.toLowerCase() }];
do {
documentPath = path.dirname(documentPath);
parentChain.push({ id: documentPath.toLowerCase() });
if (parentChain.length >= maxDepth) {
// this is an odd case that can happen when we change a crate version but we'd still have
// a open file referencing the old version
return;
}
} while (!ctx.dependencies?.contains(documentPath));
parentChain.reverse();
for (const idx in parentChain) {
await ctx.treeView?.reveal(parentChain[idx], { select: true, expand: true });
}
}
export async function execRevealDependency(e: RustEditor): Promise<void> {
await vscode.commands.executeCommand("rust-analyzer.revealDependency", e);
}
export function ssr(ctx: CtxInit): Cmd {
return async () => {
const editor = vscode.window.activeTextEditor;

View file

@ -7,6 +7,7 @@ import { Config, prepareVSCodeConfig } from "./config";
import { createClient } from "./client";
import {
executeDiscoverProject,
isDocumentInWorkspace,
isRustDocument,
isRustEditor,
LazyOutputChannel,
@ -14,6 +15,13 @@ import {
RustEditor,
} from "./util";
import { ServerStatusParams } from "./lsp_ext";
import {
Dependency,
DependencyFile,
RustDependenciesProvider,
DependencyId,
} from "./dependencies_provider";
import { execRevealDependency } from "./commands";
import { PersistentState } from "./persistent_state";
import { bootstrap } from "./bootstrap";
import { ExecOptions } from "child_process";
@ -84,11 +92,21 @@ export class Ctx {
private commandFactories: Record<string, CommandFactory>;
private commandDisposables: Disposable[];
private unlinkedFiles: vscode.Uri[];
private _dependencies: RustDependenciesProvider | undefined;
private _treeView: vscode.TreeView<Dependency | DependencyFile | DependencyId> | undefined;
get client() {
return this._client;
}
get treeView() {
return this._treeView;
}
get dependencies() {
return this._dependencies;
}
constructor(
readonly extCtx: vscode.ExtensionContext,
commandFactories: Record<string, CommandFactory>,
@ -101,7 +119,6 @@ export class Ctx {
this.commandDisposables = [];
this.commandFactories = commandFactories;
this.unlinkedFiles = [];
this.state = new PersistentState(extCtx.globalState);
this.config = new Config(extCtx);
@ -246,6 +263,53 @@ export class Ctx {
}
await client.start();
this.updateCommands();
this.prepareTreeDependenciesView(client);
}
private prepareTreeDependenciesView(client: lc.LanguageClient) {
const ctxInit: CtxInit = {
...this,
client: client,
};
this._dependencies = new RustDependenciesProvider(ctxInit);
this._treeView = vscode.window.createTreeView("rustDependencies", {
treeDataProvider: this._dependencies,
showCollapseAll: true,
});
this.pushExtCleanup(this._treeView);
vscode.window.onDidChangeActiveTextEditor(async (e) => {
// we should skip documents that belong to the current workspace
if (this.shouldRevealDependency(e)) {
try {
await execRevealDependency(e);
} catch (reason) {
await vscode.window.showErrorMessage(`Dependency error: ${reason}`);
}
}
});
this.treeView?.onDidChangeVisibility(async (e) => {
if (e.visible) {
const activeEditor = vscode.window.activeTextEditor;
if (this.shouldRevealDependency(activeEditor)) {
try {
await execRevealDependency(activeEditor);
} catch (reason) {
await vscode.window.showErrorMessage(`Dependency error: ${reason}`);
}
}
}
});
}
private shouldRevealDependency(e: vscode.TextEditor | undefined): e is RustEditor {
return (
e !== undefined &&
isRustEditor(e) &&
!isDocumentInWorkspace(e.document) &&
(this.treeView?.visible || false)
);
}
async restart() {
@ -348,6 +412,7 @@ export class Ctx {
statusBar.color = undefined;
statusBar.backgroundColor = undefined;
statusBar.command = "rust-analyzer.stopServer";
this.dependencies?.refresh();
break;
case "warning":
if (status.message) {
@ -410,4 +475,5 @@ export class Ctx {
export interface Disposable {
dispose(): void;
}
export type Cmd = (...args: any[]) => unknown;

View file

@ -0,0 +1,144 @@
import * as vscode from "vscode";
import * as fspath from "path";
import * as fs from "fs";
import { CtxInit } from "./ctx";
import * as ra from "./lsp_ext";
import { FetchDependencyListResult } from "./lsp_ext";
export class RustDependenciesProvider
implements vscode.TreeDataProvider<Dependency | DependencyFile>
{
dependenciesMap: { [id: string]: Dependency | DependencyFile };
ctx: CtxInit;
constructor(ctx: CtxInit) {
this.dependenciesMap = {};
this.ctx = ctx;
}
private _onDidChangeTreeData: vscode.EventEmitter<
Dependency | DependencyFile | undefined | null | void
> = new vscode.EventEmitter<Dependency | undefined | null | void>();
readonly onDidChangeTreeData: vscode.Event<
Dependency | DependencyFile | undefined | null | void
> = this._onDidChangeTreeData.event;
getDependency(filePath: string): Dependency | DependencyFile | undefined {
return this.dependenciesMap[filePath.toLowerCase()];
}
contains(filePath: string): boolean {
return filePath.toLowerCase() in this.dependenciesMap;
}
isInitialized(): boolean {
return Object.keys(this.dependenciesMap).length !== 0;
}
refresh(): void {
this.dependenciesMap = {};
this._onDidChangeTreeData.fire();
}
getParent?(
element: Dependency | DependencyFile
): vscode.ProviderResult<Dependency | DependencyFile> {
if (element instanceof Dependency) return undefined;
return element.parent;
}
getTreeItem(element: Dependency | DependencyFile): vscode.TreeItem | Thenable<vscode.TreeItem> {
if (element.id! in this.dependenciesMap) return this.dependenciesMap[element.id!];
return element;
}
getChildren(
element?: Dependency | DependencyFile
): vscode.ProviderResult<Dependency[] | DependencyFile[]> {
return new Promise((resolve, _reject) => {
if (!vscode.workspace.workspaceFolders) {
void vscode.window.showInformationMessage("No dependency in empty workspace");
return Promise.resolve([]);
}
if (element) {
const files = fs.readdirSync(element.dependencyPath).map((fileName) => {
const filePath = fspath.join(element.dependencyPath, fileName);
const collapsibleState = fs.lstatSync(filePath).isDirectory()
? vscode.TreeItemCollapsibleState.Collapsed
: vscode.TreeItemCollapsibleState.None;
const dep = new DependencyFile(fileName, filePath, element, collapsibleState);
this.dependenciesMap[dep.dependencyPath.toLowerCase()] = dep;
return dep;
});
return resolve(files);
} else {
return resolve(this.getRootDependencies());
}
});
}
private async getRootDependencies(): Promise<Dependency[]> {
const dependenciesResult: FetchDependencyListResult = await this.ctx.client.sendRequest(
ra.fetchDependencyList,
{}
);
const crates = dependenciesResult.crates;
return crates.map((crate) => {
const dep = this.toDep(crate.name || "unknown", crate.version || "", crate.path);
this.dependenciesMap[dep.dependencyPath.toLowerCase()] = dep;
return dep;
});
}
private toDep(moduleName: string, version: string, path: string): Dependency {
return new Dependency(
moduleName,
version,
vscode.Uri.parse(path).fsPath,
vscode.TreeItemCollapsibleState.Collapsed
);
}
}
export class Dependency extends vscode.TreeItem {
constructor(
public readonly label: string,
private version: string,
readonly dependencyPath: string,
public readonly collapsibleState: vscode.TreeItemCollapsibleState
) {
super(label, collapsibleState);
this.resourceUri = vscode.Uri.file(dependencyPath);
this.id = this.resourceUri.fsPath.toLowerCase();
this.description = this.version;
if (this.version) {
this.tooltip = `${this.label}-${this.version}`;
} else {
this.tooltip = this.label;
}
}
}
export class DependencyFile extends vscode.TreeItem {
constructor(
readonly label: string,
readonly dependencyPath: string,
readonly parent: Dependency | DependencyFile,
public readonly collapsibleState: vscode.TreeItemCollapsibleState
) {
super(vscode.Uri.file(dependencyPath), collapsibleState);
this.id = this.resourceUri!.fsPath.toLowerCase();
const isDir = fs.lstatSync(this.resourceUri!.fsPath).isDirectory();
if (!isDir) {
this.command = {
command: "vscode.open",
title: "Open File",
arguments: [this.resourceUri],
};
}
}
}
export type DependencyId = { id: string };

View file

@ -70,6 +70,38 @@ export const viewItemTree = new lc.RequestType<ViewItemTreeParams, string, void>
export type AnalyzerStatusParams = { textDocument?: lc.TextDocumentIdentifier };
export interface FetchDependencyListParams {}
export interface FetchDependencyListResult {
crates: {
name: string | undefined;
version: string | undefined;
path: string;
}[];
}
export const fetchDependencyList = new lc.RequestType<
FetchDependencyListParams,
FetchDependencyListResult,
void
>("rust-analyzer/fetchDependencyList");
export interface FetchDependencyGraphParams {}
export interface FetchDependencyGraphResult {
crates: {
name: string;
version: string;
path: string;
}[];
}
export const fetchDependencyGraph = new lc.RequestType<
FetchDependencyGraphParams,
FetchDependencyGraphResult,
void
>("rust-analyzer/fetchDependencyGraph");
export type ExpandMacroParams = {
textDocument: lc.TextDocumentIdentifier;
position: lc.Position;

View file

@ -190,5 +190,6 @@ function createCommands(): Record<string, CommandFactory> {
showReferences: { enabled: commands.showReferences },
triggerParameterHints: { enabled: commands.triggerParameterHints },
openLogs: { enabled: commands.openLogs },
revealDependency: { enabled: commands.revealDependency },
};
}

View file

@ -112,6 +112,19 @@ export function isRustEditor(editor: vscode.TextEditor): editor is RustEditor {
return isRustDocument(editor.document);
}
export function isDocumentInWorkspace(document: RustDocument): boolean {
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders) {
return false;
}
for (const folder of workspaceFolders) {
if (document.uri.fsPath.startsWith(folder.uri.fsPath)) {
return true;
}
}
return false;
}
export function isValidExecutable(path: string): boolean {
log.debug("Checking availability of a binary at", path);