@microsoft/inshellisense 0.0.1-rc.5 → 0.0.1-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,22 +1,52 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
- import { exec, spawn } from "node:child_process";
4
- export const buildExecuteShellCommand = (timeout) =>
5
- // eslint-disable-next-line @typescript-eslint/no-unused-vars -- TODO: use cwd in the future
6
- async (command, cwd) => {
7
- return new Promise((resolve) => {
8
- exec(command, { timeout }, (_, stdout, stderr) => {
9
- resolve(stdout || stderr);
10
- });
3
+ import path from "node:path";
4
+ import { spawn } from "node:child_process";
5
+ import fsAsync from "node:fs/promises";
6
+ import { Shell } from "../utils/shell.js";
7
+ import log from "../utils/log.js";
8
+ export const buildExecuteShellCommand = (timeout) => async ({ command, env, args, cwd }) => {
9
+ const child = spawn(command, args, { cwd, env });
10
+ setTimeout(() => child.kill("SIGKILL"), timeout);
11
+ let stdout = "";
12
+ let stderr = "";
13
+ child.stdout.on("data", (data) => (stdout += data));
14
+ child.stderr.on("data", (data) => (stderr += data));
15
+ child.on("error", (err) => {
16
+ log.debug({ msg: "shell command failed", e: err.message });
11
17
  });
12
- };
13
- export const executeShellCommandTTY = async (shell, command) => {
14
- const child = spawn(shell, ["-c", command.trim()], { stdio: "inherit" });
15
18
  return new Promise((resolve) => {
16
19
  child.on("close", (code) => {
17
20
  resolve({
18
- code,
21
+ status: code ?? 0,
22
+ stderr,
23
+ stdout,
19
24
  });
20
25
  });
21
26
  });
22
27
  };
28
+ export const resolveCwd = async (cmdToken, cwd, shell) => {
29
+ if (cmdToken == null)
30
+ return { cwd, pathy: false, complete: false };
31
+ const { token } = cmdToken;
32
+ const sep = shell == Shell.Bash ? "/" : path.sep;
33
+ if (!token.includes(sep))
34
+ return { cwd, pathy: false, complete: false };
35
+ const resolvedCwd = path.isAbsolute(token) ? token : path.join(cwd, token);
36
+ try {
37
+ await fsAsync.access(resolvedCwd, fsAsync.constants.R_OK);
38
+ return { cwd: resolvedCwd, pathy: true, complete: token.endsWith(sep) };
39
+ }
40
+ catch {
41
+ // fallback to the parent folder if possible
42
+ const baselessCwd = resolvedCwd.substring(0, resolvedCwd.length - path.basename(resolvedCwd).length);
43
+ try {
44
+ await fsAsync.access(baselessCwd, fsAsync.constants.R_OK);
45
+ return { cwd: baselessCwd, pathy: true, complete: token.endsWith(sep) };
46
+ }
47
+ catch {
48
+ /*empty*/
49
+ }
50
+ return { cwd, pathy: false, complete: false };
51
+ }
52
+ };
@@ -17,11 +17,13 @@ export class SuggestionManager {
17
17
  #command;
18
18
  #activeSuggestionIdx;
19
19
  #suggestBlob;
20
- constructor(terminal) {
20
+ #shell;
21
+ constructor(terminal, shell) {
21
22
  this.#term = terminal;
22
23
  this.#suggestBlob = { suggestions: [] };
23
24
  this.#command = "";
24
25
  this.#activeSuggestionIdx = 0;
26
+ this.#shell = shell;
25
27
  }
26
28
  async _loadSuggestions() {
27
29
  const commandText = this.#term.getCommandState().commandText;
@@ -33,7 +35,7 @@ export class SuggestionManager {
33
35
  return;
34
36
  }
35
37
  this.#command = commandText;
36
- const suggestionBlob = await getSuggestions(commandText);
38
+ const suggestionBlob = await getSuggestions(commandText, this.#term.cwd, this.#shell);
37
39
  this.#suggestBlob = suggestionBlob;
38
40
  }
39
41
  _renderArgumentDescription(description, x) {
@@ -46,6 +48,11 @@ export class SuggestionManager {
46
48
  return "";
47
49
  return renderBox(truncateMultilineText(description, descriptionWidth - borderWidth, descriptionHeight), descriptionWidth, x);
48
50
  }
51
+ _descriptionRows(description) {
52
+ if (!description)
53
+ return 0;
54
+ return truncateMultilineText(description, descriptionWidth - borderWidth, descriptionHeight).length;
55
+ }
49
56
  _renderSuggestions(suggestions, activeSuggestionIdx, x) {
50
57
  return renderBox(suggestions.map((suggestion, idx) => {
51
58
  const suggestionText = `${suggestion.icon} ${suggestion.name}`.padEnd(suggestionWidth - borderWidth, " ");
@@ -53,10 +60,10 @@ export class SuggestionManager {
53
60
  return idx == activeSuggestionIdx ? chalk.bgHex(activeSuggestionBackgroundColor)(truncatedSuggestion) : truncatedSuggestion;
54
61
  }), suggestionWidth, x);
55
62
  }
56
- async render() {
63
+ async render(remainingLines) {
57
64
  await this._loadSuggestions();
58
65
  if (!this.#suggestBlob)
59
- return { data: "", columns: 0 };
66
+ return { data: "", rows: 0 };
60
67
  const { suggestions, argumentDescription } = this.#suggestBlob;
61
68
  const page = Math.min(Math.floor(this.#activeSuggestionIdx / maxSuggestions) + 1, Math.floor(suggestions.length / maxSuggestions) + 1);
62
69
  const pagedSuggestions = suggestions.filter((_, idx) => idx < page * maxSuggestions && idx >= (page - 1) * maxSuggestions);
@@ -77,20 +84,32 @@ export class SuggestionManager {
77
84
  ansi.cursorUp(2) +
78
85
  ansi.cursorForward(clampedLeftPadding) +
79
86
  this._renderArgumentDescription(argumentDescription, clampedLeftPadding),
80
- columns: 3,
87
+ rows: 3,
81
88
  };
82
89
  }
83
- return { data: "", columns: 0 };
90
+ return { data: "", rows: 0 };
91
+ }
92
+ const suggestionRowsUsed = pagedSuggestions.length + borderWidth;
93
+ let descriptionRowsUsed = this._descriptionRows(activeDescription) + borderWidth;
94
+ let rows = Math.max(descriptionRowsUsed, suggestionRowsUsed);
95
+ if (rows <= remainingLines) {
96
+ descriptionRowsUsed = suggestionRowsUsed;
97
+ rows = suggestionRowsUsed;
84
98
  }
85
- const columnsUsed = pagedSuggestions.length + borderWidth;
86
- const ui = swapDescription
87
- ? this._renderDescription(activeDescription, clampedLeftPadding) +
88
- this._renderSuggestions(pagedSuggestions, activePagedSuggestionIndex, clampedLeftPadding + descriptionWidth)
89
- : this._renderSuggestions(pagedSuggestions, activePagedSuggestionIndex, clampedLeftPadding) +
90
- this._renderDescription(activeDescription, clampedLeftPadding + suggestionWidth);
99
+ const descriptionUI = ansi.cursorUp(descriptionRowsUsed - 1) +
100
+ (swapDescription
101
+ ? this._renderDescription(activeDescription, clampedLeftPadding)
102
+ : this._renderDescription(activeDescription, clampedLeftPadding + suggestionWidth)) +
103
+ ansi.cursorDown(descriptionRowsUsed - 1);
104
+ const suggestionUI = ansi.cursorUp(suggestionRowsUsed - 1) +
105
+ (swapDescription
106
+ ? this._renderSuggestions(pagedSuggestions, activePagedSuggestionIndex, clampedLeftPadding + descriptionWidth)
107
+ : this._renderSuggestions(pagedSuggestions, activePagedSuggestionIndex, clampedLeftPadding)) +
108
+ ansi.cursorDown(suggestionRowsUsed - 1);
109
+ const ui = swapDescription ? descriptionUI + suggestionUI : suggestionUI + descriptionUI;
91
110
  return {
92
- data: ansi.cursorHide + ansi.cursorUp(columnsUsed - 1) + ansi.cursorForward(clampedLeftPadding) + ui + ansi.cursorShow,
93
- columns: columnsUsed,
111
+ data: ansi.cursorHide + ansi.cursorForward(clampedLeftPadding) + ui + ansi.cursorShow,
112
+ rows,
94
113
  };
95
114
  }
96
115
  update(input) {
@@ -108,7 +127,8 @@ export class SuggestionManager {
108
127
  }
109
128
  else if (keyStroke == "tab") {
110
129
  const removals = "\u007F".repeat(this.#suggestBlob?.charactersToDrop ?? 0);
111
- const chars = this.#suggestBlob?.suggestions.at(this.#activeSuggestionIdx)?.name + " ";
130
+ const suggestion = this.#suggestBlob?.suggestions.at(this.#activeSuggestionIdx);
131
+ const chars = suggestion?.insertValue ?? suggestion?.name + " ";
112
132
  if (this.#suggestBlob == null || !chars.trim() || this.#suggestBlob?.suggestions.length == 0) {
113
133
  return false;
114
134
  }
@@ -1,16 +1,21 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
+ import ansi from "ansi-escapes";
4
+ import chalk from "chalk";
3
5
  import { inputModifier } from "./input.js";
4
6
  import log from "../utils/log.js";
5
7
  import isterm from "../isterm/index.js";
6
8
  import { eraseLinesBelow } from "../utils/ansi.js";
7
- import ansi from "ansi-escapes";
8
9
  import { SuggestionManager, MAX_LINES } from "./suggestionManager.js";
10
+ export const renderConfirmation = (live) => {
11
+ const statusMessage = live ? chalk.green("live") : chalk.red("not found");
12
+ return `inshellisense session [${statusMessage}]\n`;
13
+ };
9
14
  export const render = async (shell) => {
10
15
  const term = await isterm.spawn({ shell, rows: process.stdout.rows, cols: process.stdout.columns });
11
- const suggestionManager = new SuggestionManager(term);
16
+ const suggestionManager = new SuggestionManager(term, shell);
12
17
  let hasActiveSuggestions = false;
13
- let previousSuggestionsColumns = 0;
18
+ let previousSuggestionsRows = 0;
14
19
  process.stdin.setRawMode(true);
15
20
  const writeOutput = (data) => {
16
21
  log.debug({ msg: "writing data", data });
@@ -18,30 +23,36 @@ export const render = async (shell) => {
18
23
  };
19
24
  writeOutput(ansi.clearTerminal);
20
25
  term.onData((data) => {
21
- const commandState = term.getCommandState();
22
- if ((commandState.hasOutput || hasActiveSuggestions) && !commandState.persistentOutput) {
23
- if (term.getCursorState().remainingLines < previousSuggestionsColumns) {
24
- writeOutput(ansi.cursorHide +
26
+ if (hasActiveSuggestions) {
27
+ // Considers when data includes newlines which have shifted the cursor position downwards
28
+ const newlines = Math.max((data.match(/\r/g) || []).length, (data.match(/\n/g) || []).length);
29
+ const linesOfInterest = MAX_LINES + newlines;
30
+ if (term.getCursorState().remainingLines <= previousSuggestionsRows) {
31
+ // handles when suggestions get loaded before shell output so you need to always clear below output as a precaution
32
+ if (term.getCursorState().remainingLines != 0) {
33
+ writeOutput(ansi.cursorHide + ansi.cursorSavePosition + eraseLinesBelow(linesOfInterest + 1) + ansi.cursorRestorePosition);
34
+ }
35
+ writeOutput(data +
36
+ ansi.cursorHide +
25
37
  ansi.cursorSavePosition +
26
- ansi.cursorPrevLine.repeat(MAX_LINES) +
27
- term.getCells(MAX_LINES, "above") +
38
+ ansi.cursorPrevLine.repeat(linesOfInterest) +
39
+ term.getCells(linesOfInterest, "above") +
28
40
  ansi.cursorRestorePosition +
29
- ansi.cursorShow +
30
- data);
41
+ ansi.cursorShow);
31
42
  }
32
43
  else {
33
- writeOutput(ansi.cursorHide + ansi.cursorSavePosition + eraseLinesBelow(MAX_LINES + 1) + ansi.cursorRestorePosition + ansi.cursorShow + data);
44
+ writeOutput(ansi.cursorHide + ansi.cursorSavePosition + eraseLinesBelow(linesOfInterest + 1) + ansi.cursorRestorePosition + ansi.cursorShow + data);
34
45
  }
35
46
  }
36
47
  else {
37
48
  writeOutput(data);
38
49
  }
39
50
  setImmediate(async () => {
40
- const suggestion = await suggestionManager.render();
51
+ const suggestion = await suggestionManager.render(term.getCursorState().remainingLines);
41
52
  const commandState = term.getCommandState();
42
53
  if (suggestion.data != "" && commandState.cursorTerminated && !commandState.hasOutput) {
43
54
  if (hasActiveSuggestions) {
44
- if (term.getCursorState().remainingLines < suggestion.columns) {
55
+ if (term.getCursorState().remainingLines < suggestion.rows) {
45
56
  writeOutput(ansi.cursorHide +
46
57
  ansi.cursorSavePosition +
47
58
  ansi.cursorPrevLine.repeat(MAX_LINES) +
@@ -54,7 +65,7 @@ export const render = async (shell) => {
54
65
  ansi.cursorShow);
55
66
  }
56
67
  else {
57
- const offset = MAX_LINES - suggestion.columns;
68
+ const offset = MAX_LINES - suggestion.rows;
58
69
  writeOutput(ansi.cursorHide +
59
70
  ansi.cursorSavePosition +
60
71
  eraseLinesBelow(MAX_LINES) +
@@ -65,13 +76,13 @@ export const render = async (shell) => {
65
76
  }
66
77
  }
67
78
  else {
68
- if (term.getCursorState().remainingLines < suggestion.columns) {
79
+ if (term.getCursorState().remainingLines < suggestion.rows) {
69
80
  writeOutput(ansi.cursorHide + ansi.cursorSavePosition + ansi.cursorUp() + suggestion.data + ansi.cursorRestorePosition + ansi.cursorShow);
70
81
  }
71
82
  else {
72
83
  writeOutput(ansi.cursorHide +
73
84
  ansi.cursorSavePosition +
74
- ansi.cursorNextLine.repeat(suggestion.columns) +
85
+ ansi.cursorNextLine.repeat(suggestion.rows) +
75
86
  suggestion.data +
76
87
  ansi.cursorRestorePosition +
77
88
  ansi.cursorShow);
@@ -81,7 +92,7 @@ export const render = async (shell) => {
81
92
  }
82
93
  else {
83
94
  if (hasActiveSuggestions) {
84
- if (term.getCursorState().remainingLines < previousSuggestionsColumns) {
95
+ if (term.getCursorState().remainingLines <= previousSuggestionsRows) {
85
96
  writeOutput(ansi.cursorHide +
86
97
  ansi.cursorSavePosition +
87
98
  ansi.cursorPrevLine.repeat(MAX_LINES) +
@@ -95,12 +106,12 @@ export const render = async (shell) => {
95
106
  }
96
107
  hasActiveSuggestions = false;
97
108
  }
98
- previousSuggestionsColumns = suggestion.columns;
109
+ previousSuggestionsRows = suggestion.rows;
99
110
  });
100
111
  });
101
112
  process.stdin.on("data", (d) => {
102
113
  const suggestionResult = suggestionManager.update(d);
103
- if (previousSuggestionsColumns > 0 && suggestionResult == "handled") {
114
+ if (previousSuggestionsRows > 0 && suggestionResult == "handled") {
104
115
  term.noop();
105
116
  }
106
117
  else if (suggestionResult != "fully-handled") {
@@ -11,6 +11,7 @@ export var IstermOscPt;
11
11
  (function (IstermOscPt) {
12
12
  IstermOscPt["PromptStarted"] = "PS";
13
13
  IstermOscPt["PromptEnded"] = "PE";
14
+ IstermOscPt["CurrentWorkingDirectory"] = "CWD";
14
15
  })(IstermOscPt || (IstermOscPt = {}));
15
16
  export const IstermPromptStart = IS_OSC + IstermOscPt.PromptStarted + BEL;
16
17
  export const IstermPromptEnd = IS_OSC + IstermOscPt.PromptEnded + BEL;
@@ -4,9 +4,13 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
  import fs from "node:fs";
6
6
  import fsAsync from "node:fs/promises";
7
- const logTarget = path.join(os.homedir(), ".inshellisense", "inshellisense.log");
8
- const logEnabled = false;
7
+ const logFolder = path.join(os.homedir(), ".inshellisense");
8
+ const logTarget = path.join(logFolder, "inshellisense.log");
9
+ let logEnabled = false;
9
10
  const reset = async () => {
11
+ if (!fs.existsSync(logTarget)) {
12
+ await fsAsync.mkdir(logFolder, { recursive: true });
13
+ }
10
14
  await fsAsync.writeFile(logTarget, "");
11
15
  };
12
16
  const debug = (content) => {
@@ -19,4 +23,8 @@ const debug = (content) => {
19
23
  }
20
24
  });
21
25
  };
22
- export default { reset, debug };
26
+ export const enable = async () => {
27
+ await reset();
28
+ logEnabled = true;
29
+ };
30
+ export default { reset, debug, enable };
@@ -17,9 +17,25 @@ export var Shell;
17
17
  Shell["Fish"] = "fish";
18
18
  Shell["Cmd"] = "cmd";
19
19
  })(Shell || (Shell = {}));
20
- export const supportedShells = [Shell.Bash, process.platform == "win32" ? Shell.Powershell : null, Shell.Pwsh, Shell.Zsh, Shell.Fish].filter((shell) => shell != null);
20
+ export const supportedShells = [
21
+ Shell.Bash,
22
+ process.platform == "win32" ? Shell.Powershell : null,
23
+ Shell.Pwsh,
24
+ Shell.Zsh,
25
+ Shell.Fish,
26
+ process.platform == "win32" ? Shell.Cmd : null,
27
+ ].filter((shell) => shell != null);
21
28
  export const userZdotdir = process.env?.ZDOTDIR ?? os.homedir() ?? `~`;
22
29
  export const zdotdir = path.join(os.tmpdir(), `is-zsh`);
30
+ const configFolder = ".inshellisense";
31
+ export const setupBashPreExec = async () => {
32
+ const shellFolderPath = path.join(path.dirname(url.fileURLToPath(import.meta.url)), "..", "..", "shell");
33
+ const globalConfigPath = path.join(os.homedir(), configFolder);
34
+ if (!fs.existsSync(globalConfigPath)) {
35
+ await fsAsync.mkdir(globalConfigPath, { recursive: true });
36
+ }
37
+ await fsAsync.cp(path.join(shellFolderPath, "bash-preexec.sh"), path.join(globalConfigPath, "bash-preexec.sh"));
38
+ };
23
39
  export const setupZshDotfiles = async () => {
24
40
  const shellFolderPath = path.join(path.dirname(url.fileURLToPath(import.meta.url)), "..", "..", "shell");
25
41
  await fsAsync.cp(path.join(shellFolderPath, "shellIntegration-rc.zsh"), path.join(zdotdir, ".zshrc"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microsoft/inshellisense",
3
- "version": "0.0.1-rc.5",
3
+ "version": "0.0.1-rc.6",
4
4
  "description": "IDE style command line auto complete",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,8 @@
18
18
  "start": "node ./build/index.js",
19
19
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
20
20
  "lint": "eslint src/ --ext .ts,.tsx && prettier src/ --check",
21
- "lint:fix": "eslint src/ --ext .ts,.tsx --fix && prettier src/ --write"
21
+ "lint:fix": "eslint src/ --ext .ts,.tsx --fix && prettier src/ --write",
22
+ "debug": "node --inspect --loader ts-node/esm src/index.ts -V"
22
23
  },
23
24
  "repository": {
24
25
  "type": "git",
@@ -60,6 +61,7 @@
60
61
  "jest": "^29.7.0",
61
62
  "prettier": "3.0.3",
62
63
  "ts-jest": "^29.1.1",
64
+ "ts-node": "^10.9.2",
63
65
  "typescript": "^5.2.2"
64
66
  }
65
67
  }