Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@ inshellisense supports the following shells:

## Configuration

All configuration is done through a [toml](https://toml.io/) file. You can create this file at `~/.inshellisenserc` or, for XDG compliance, at `~/.config/inshellisense/rc.toml`. The [JSON schema](https://json-schema.org/) for the configuration file can be found [here](https://github.com/microsoft/inshellisense/blob/main/src/utils/config.ts).
All configuration is done through a [toml](https://toml.io/) file. You can create this file at `~/.inshellisenserc` or `$XDG_CONFIG_HOME/inshellisense/rc.toml`. When `XDG_CONFIG_HOME` is unset, empty, or not an absolute path, the XDG configuration path defaults to `~/.config/inshellisense/rc.toml`. The [JSON schema](https://json-schema.org/) for the configuration file can be found [here](https://github.com/microsoft/inshellisense/blob/main/src/utils/config.ts).

On new Unix-like installations, generated resources are stored under `$XDG_DATA_HOME/inshellisense`. When `XDG_DATA_HOME` is unset, empty, or non-absolute, it defaults to `~/.local/share` as defined by the XDG specification. Existing installations continue using `~/.inshellisense` when that directory is present so current shell plugins keep working; Windows also keeps using that location. Running `is reinit` migrates legacy resources to the XDG data directory.

### Keybindings

Expand Down
6 changes: 5 additions & 1 deletion scripts/pkg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,11 @@ const applyBundlePatches = async (): Promise<void> => {
'var dirs = ["build/Release", "build/Debug", "prebuilds/" + process.platform + "-" + process.arch];',
`var os_1 = require("os");
var path_1 = require("path");
var dirs = [path_1.join(os_1.homedir(), ".inshellisense", "native"), "build/Release", "build/Debug", "prebuilds/" + process.platform + "-" + process.arch];`,
var fs_1 = require("fs");
var legacy_resources_dir = path_1.join(os_1.homedir(), ".inshellisense");
var xdg_data_home = process.platform !== "win32" ? (path_1.isAbsolute(process.env.XDG_DATA_HOME || "") ? process.env.XDG_DATA_HOME : path_1.join(os_1.homedir(), ".local", "share")) : void 0;
var resources_dir = xdg_data_home && !fs_1.existsSync(legacy_resources_dir) ? path_1.join(xdg_data_home, "inshellisense") : legacy_resources_dir;
var dirs = [path_1.join(resources_dir, "native"), "build/Release", "build/Debug", "prebuilds/" + process.platform + "-" + process.arch];`,
"native locations",
);

Expand Down
4 changes: 2 additions & 2 deletions shell/shellIntegration.bash
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ else
fi
fi

if [ -r ~/.inshellisense/shell/bash-preexec.sh ]; then
. ~/.inshellisense/shell/bash-preexec.sh
if [ -r "${BASH_SOURCE[0]%/*}/bash-preexec.sh" ]; then

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will break git bash on windows due to the different path separator. using cygpath to convert into a unix style path and then sourcing the preexec should fix the issue

__is_shell_source="${BASH_SOURCE[0]}"
if command -v cygpath >/dev/null 2>&1; then
	__is_shell_source=$(cygpath -u "$__is_shell_source")
fi

if [ -r "${__is_shell_source%/*}/bash-preexec.sh" ]; then
	. "${__is_shell_source%/*}/bash-preexec.sh"
fi

. "${BASH_SOURCE[0]%/*}/bash-preexec.sh"
fi

__is_prompt_start() {
Expand Down
61 changes: 61 additions & 0 deletions src/tests/utils/constants.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import path from "node:path";
import { resolveConfigFilePath, resolveResourcesPath, resolveXdgConfigHome, resolveXdgDataHome } from "../../utils/constants.js";

const homeDirectory = path.join(path.sep, "home", "tester");
const xdgConfigDirectory = path.join(path.sep, "tmp", "xdg");
const xdgDataDirectory = path.join(path.sep, "tmp", "xdg-data");

describe("resolveXdgConfigHome", () => {
test("uses an absolute XDG config directory on Unix", () => {
expect(resolveXdgConfigHome(xdgConfigDirectory, "linux")).toBe(xdgConfigDirectory);
});

test.each([undefined, "", "relative/xdg", "~/.config"])("ignores an unset, empty, or non-absolute XDG config directory", (value) => {
expect(resolveXdgConfigHome(value, "linux")).toBeUndefined();
});

test("preserves the legacy location on Windows", () => {
expect(resolveXdgConfigHome(xdgConfigDirectory, "win32")).toBeUndefined();
});
});

describe("resolveResourcesPath", () => {
test("uses the legacy hidden directory without XDG_CONFIG_HOME", () => {
expect(resolveResourcesPath(homeDirectory, undefined, false)).toBe(path.join(homeDirectory, ".inshellisense"));
});

test("uses an unhidden directory below XDG_DATA_HOME for a new installation", () => {
expect(resolveResourcesPath(homeDirectory, xdgDataDirectory, false)).toBe(path.join(xdgDataDirectory, "inshellisense"));
});

test("preserves an existing legacy resource directory", () => {
expect(resolveResourcesPath(homeDirectory, xdgDataDirectory, true)).toBe(path.join(homeDirectory, ".inshellisense"));
});
});

describe("resolveXdgDataHome", () => {
test("uses an absolute XDG data directory on Unix", () => {
expect(resolveXdgDataHome(xdgDataDirectory, homeDirectory, "linux")).toBe(xdgDataDirectory);
});

test.each([undefined, "", "relative/xdg", "~/.local/share"])("uses the XDG default for an unset, empty, or non-absolute data directory", (value) => {
expect(resolveXdgDataHome(value, homeDirectory, "linux")).toBe(path.join(homeDirectory, ".local", "share"));
});

test("preserves the legacy location on Windows", () => {
expect(resolveXdgDataHome(xdgDataDirectory, homeDirectory, "win32")).toBeUndefined();
});
});

describe("resolveConfigFilePath", () => {
test("uses the XDG default below the home directory", () => {
expect(resolveConfigFilePath(homeDirectory, undefined)).toBe(path.join(homeDirectory, ".config", "inshellisense", "rc.toml"));
});

test("uses XDG_CONFIG_HOME when configured", () => {
expect(resolveConfigFilePath(homeDirectory, xdgConfigDirectory)).toBe(path.join(xdgConfigDirectory, "inshellisense", "rc.toml"));
});
});
84 changes: 84 additions & 0 deletions src/tests/utils/shell.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { getShellSourceCommand, hasLegacyShellConfig, Shell } from "../../utils/shell.js";

describe("getShellSourceCommand", () => {
test.each([
[Shell.Bash, "~/.inshellisense/init/bash/init.sh", "[ -f ~/.inshellisense/init/bash/init.sh ] && source ~/.inshellisense/init/bash/init.sh"],
[
Shell.Powershell,
"~/.inshellisense/init/powershell/init.ps1",
"if ( Test-Path '~/.inshellisense/init/powershell/init.ps1' -PathType Leaf ) { . ~/.inshellisense/init/powershell/init.ps1 }",
],
[
Shell.Pwsh,
"~/.inshellisense/init/pwsh/init.ps1",
"if ( Test-Path '~/.inshellisense/init/pwsh/init.ps1' -PathType Leaf ) { . ~/.inshellisense/init/pwsh/init.ps1 }",
],
[Shell.Zsh, "~/.inshellisense/init/zsh/init.zsh", "[[ -f ~/.inshellisense/init/zsh/init.zsh ]] && source ~/.inshellisense/init/zsh/init.zsh"],
[Shell.Fish, "~/.inshellisense/init/fish/init.fish", "test -f ~/.inshellisense/init/fish/init.fish && source ~/.inshellisense/init/fish/init.fish"],
[Shell.Xonsh, "~/.inshellisense/init/xonsh/init.xsh", 'p"~/.inshellisense/init/xonsh/init.xsh".exists() && source "~/.inshellisense/init/xonsh/init.xsh"'],
[Shell.Nushell, "~/.inshellisense/init/nu/init.nu", "if ( '~/.inshellisense/init/nu/init.nu' | path exists ) { source ~/.inshellisense/init/nu/init.nu }"],
])("preserves the legacy %s command", (shell, initFilePath, expected) => {
expect(getShellSourceCommand(shell, initFilePath)).toBe(expected);
});

test.each([
[
Shell.Bash,
"/tmp/xdg home/inshellisense/init/bash/init.sh",
"[ -f '/tmp/xdg home/inshellisense/init/bash/init.sh' ] && source '/tmp/xdg home/inshellisense/init/bash/init.sh'",
],
[
Shell.Pwsh,
"/tmp/xdg home/inshellisense/init/pwsh/init.ps1",
"if ( Test-Path '/tmp/xdg home/inshellisense/init/pwsh/init.ps1' -PathType Leaf ) { . '/tmp/xdg home/inshellisense/init/pwsh/init.ps1' }",
],
[
Shell.Zsh,
"/tmp/xdg home/inshellisense/init/zsh/init.zsh",
"[[ -f '/tmp/xdg home/inshellisense/init/zsh/init.zsh' ]] && source '/tmp/xdg home/inshellisense/init/zsh/init.zsh'",
],
[
Shell.Fish,
"/tmp/xdg home/inshellisense/init/fish/init.fish",
"test -f '/tmp/xdg home/inshellisense/init/fish/init.fish' && source '/tmp/xdg home/inshellisense/init/fish/init.fish'",
],
[
Shell.Xonsh,
"/tmp/xdg home/inshellisense/init/xonsh/init.xsh",
'p"/tmp/xdg home/inshellisense/init/xonsh/init.xsh".exists() && source "/tmp/xdg home/inshellisense/init/xonsh/init.xsh"',
],
[
Shell.Nushell,
"/tmp/xdg home/inshellisense/init/nu/init.nu",
'if ( "/tmp/xdg home/inshellisense/init/nu/init.nu" | path exists ) { source "/tmp/xdg home/inshellisense/init/nu/init.nu" }',
],
])("quotes an XDG path for %s", (shell, initFilePath, expected) => {
expect(getShellSourceCommand(shell, initFilePath)).toBe(expected);
});

test("escapes shell-specific quote characters", () => {
expect(getShellSourceCommand(Shell.Bash, "/tmp/user's config/init.sh")).toContain("'/tmp/user'\\''s config/init.sh'");
expect(getShellSourceCommand(Shell.Pwsh, "/tmp/user's config/init.ps1")).toContain("'/tmp/user''s config/init.ps1'");
});
});

describe("hasLegacyShellConfig", () => {
test("detects the original shell plugin marker", () => {
expect(hasLegacyShellConfig("# inshellisense shell plugin", Shell.Zsh, false)).toBe(true);
});

test("detects the original generated plugin path", () => {
expect(hasLegacyShellConfig("source ~/.inshellisense/zsh/init.zsh", Shell.Zsh, false)).toBe(true);
});

test("detects the current legacy path after resources migrate", () => {
expect(hasLegacyShellConfig("source ~/.inshellisense/init/zsh/init.zsh", Shell.Zsh, true)).toBe(true);
});

test("allows the current legacy path before resources migrate", () => {
expect(hasLegacyShellConfig("source ~/.inshellisense/init/zsh/init.zsh", Shell.Zsh, false)).toBe(false);
});
});
32 changes: 16 additions & 16 deletions src/ui/ui-reinit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,27 @@
import chalk from "chalk";
import { unpackResources } from "../utils/node.js";
import { createShellConfigs } from "../utils/shell.js";
import {
shellResourcesPath,
nativeResourcesPath,
loggingResourcesPath,
initResourcesPath,
specResourcesPath,
versionResourcePath,
} from "../utils/constants.js";
import { allResourcesPath, getResourcePaths, preferredResourcesPath } from "../utils/constants.js";
import fs from "node:fs";

const removeResources = (resourcesPath: string) => {
const resources = getResourcePaths(resourcesPath);
fs.rmSync(resources.shell, { recursive: true, force: true });
fs.rmSync(resources.native, { recursive: true, force: true });
fs.rmSync(resources.logging, { recursive: true, force: true });
fs.rmSync(resources.init, { recursive: true, force: true });
fs.rmSync(resources.spec, { recursive: true, force: true });
fs.rmSync(resources.version, { force: true });
};

export const render = async () => {
fs.rmSync(shellResourcesPath, { recursive: true, force: true });
fs.rmSync(nativeResourcesPath, { recursive: true, force: true });
fs.rmSync(loggingResourcesPath, { recursive: true, force: true });
fs.rmSync(initResourcesPath, { recursive: true, force: true });
fs.rmSync(specResourcesPath, { recursive: true, force: true });
fs.rmSync(versionResourcePath, { force: true });
if (allResourcesPath !== preferredResourcesPath) fs.rmSync(allResourcesPath, { recursive: true, force: true });
removeResources(preferredResourcesPath);
process.stdout.write(chalk.green("✓") + " removed old inshellisense resources \n");

await createShellConfigs();
await unpackResources();
const preferredResources = getResourcePaths(preferredResourcesPath);
await createShellConfigs(preferredResources.init);
await unpackResources(preferredResourcesPath);

process.stdout.write(chalk.green("✓") + " successfully installed inshellisense \n");
};
4 changes: 2 additions & 2 deletions src/ui/ui-uninstall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { deleteCacheFolder } from "../utils/config.js";

export const render = async () => {
deleteCacheFolder();
process.stdout.write(chalk.green("✓") + " successfully deleted the .inshellisense cache folder \n");
process.stdout.write(chalk.green("✓") + " successfully deleted the inshellisense resources folder \n");
process.stdout.write(
chalk.magenta("•") + " to complete the uninstall, run the the command: " + chalk.underline(chalk.cyan("npm uninstall -g @microsoft/inshellisense")) + "\n",
chalk.magenta("•") + " to complete the uninstall, run the command: " + chalk.underline(chalk.cyan("npm uninstall -g @microsoft/inshellisense")) + "\n",
);
};
6 changes: 2 additions & 4 deletions src/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import fsAsync from "node:fs/promises";
import toml from "toml";
import _Ajv, { JSONSchemaType } from "ajv";
import { Command } from "commander";
import { allResourcesPath } from "./constants.js";
import { allResourcesPath, xdgConfigPath } from "./constants.js";

const Ajv = _Ajv as unknown as typeof _Ajv.default;
const ajv = new Ajv();
Expand Down Expand Up @@ -92,11 +92,9 @@ const configSchema = {
};

const rcFile = ".inshellisenserc";
const xdgFile = "rc.toml";
const rcPath = path.join(os.homedir(), rcFile);
const xdgPath = path.join(os.homedir(), ".config", "inshellisense", xdgFile);

const configPaths = [rcPath, xdgPath];
const configPaths = [rcPath, xdgConfigPath];

let globalConfig: Config = {
bindings: {
Expand Down
55 changes: 47 additions & 8 deletions src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,51 @@

import path from "node:path";
import os from "node:os";
import fs from "node:fs";

const inshellisenseFolderName = ".inshellisense";
export const allResourcesPath = path.join(os.homedir(), inshellisenseFolderName);
export const loggingResourcesPath = path.join(os.homedir(), inshellisenseFolderName, "log");
export const nativeResourcesPath = path.join(os.homedir(), inshellisenseFolderName, "native");
export const shellResourcesPath = path.join(os.homedir(), inshellisenseFolderName, "shell");
export const specResourcesPath = path.join(os.homedir(), inshellisenseFolderName, "spec");
export const initResourcesPath = path.join(os.homedir(), inshellisenseFolderName, "init");
export const versionResourcePath = path.join(os.homedir(), inshellisenseFolderName, "version.txt");
const inshellisenseFolderName = "inshellisense";

export const resolveXdgConfigHome = (value: string | undefined, platform: NodeJS.Platform): string | undefined => {
return platform !== "win32" && value != null && path.isAbsolute(value) ? value : undefined;
};

export const resolveXdgDataHome = (value: string | undefined, homeDirectory: string, platform: NodeJS.Platform): string | undefined => {
if (platform === "win32") return;
return value != null && path.isAbsolute(value) ? value : path.join(homeDirectory, ".local", "share");
};

export const resolveResourcesPath = (homeDirectory: string, xdgDataDirectory: string | undefined, hasLegacyResources: boolean): string => {
return xdgDataDirectory == null || hasLegacyResources
? path.join(homeDirectory, `.${inshellisenseFolderName}`)
: path.join(xdgDataDirectory, inshellisenseFolderName);
};

export const resolveConfigFilePath = (homeDirectory: string, xdgConfigDirectory: string | undefined): string => {
const configDirectory = xdgConfigDirectory ?? path.join(homeDirectory, ".config");
return path.join(configDirectory, inshellisenseFolderName, "rc.toml");
};

export const getResourcePaths = (resourcesPath: string) => ({
logging: path.join(resourcesPath, "log"),
native: path.join(resourcesPath, "native"),
shell: path.join(resourcesPath, "shell"),
spec: path.join(resourcesPath, "spec"),
init: path.join(resourcesPath, "init"),
version: path.join(resourcesPath, "version.txt"),
});

const homeDirectory = os.homedir();
const legacyResourcesPath = path.join(homeDirectory, `.${inshellisenseFolderName}`);
export const xdgConfigHome = resolveXdgConfigHome(process.env.XDG_CONFIG_HOME, process.platform);
export const xdgDataHome = resolveXdgDataHome(process.env.XDG_DATA_HOME, homeDirectory, process.platform);
export const preferredResourcesPath = resolveResourcesPath(homeDirectory, xdgDataHome, false);
export const allResourcesPath = resolveResourcesPath(homeDirectory, xdgDataHome, fs.existsSync(legacyResourcesPath));
export const usesLegacyResources = allResourcesPath === legacyResourcesPath;
export const xdgConfigPath = resolveConfigFilePath(homeDirectory, xdgConfigHome);
const resourcePaths = getResourcePaths(allResourcesPath);
export const loggingResourcesPath = resourcePaths.logging;
export const nativeResourcesPath = resourcePaths.native;
export const shellResourcesPath = resourcePaths.shell;
export const specResourcesPath = resourcePaths.spec;
export const initResourcesPath = resourcePaths.init;
export const versionResourcePath = resourcePaths.version;
Loading