@microsoft/inshellisense 0.0.1-rc.14 → 0.0.1-rc.15

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.
package/README.md CHANGED
@@ -81,7 +81,7 @@ inshellisense supports the following shells:
81
81
 
82
82
  ## Configuration
83
83
 
84
- All configuration is done through a [toml](https://toml.io/) file located at `~/.inshellisenserc`. 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).
84
+ 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).
85
85
 
86
86
  ### Keybindings
87
87
 
@@ -106,18 +106,6 @@ key = "escape"
106
106
 
107
107
  Key names are matched against the Node.js [keypress](https://nodejs.org/api/readline.html#readlineemitkeypresseventsstream-interface) events.
108
108
 
109
- ### Custom Prompts (Windows)
110
-
111
- If you are using a custom prompt in your shell (anything that is not the default PS1), you will need to set up a custom prompt in the inshellisense config file. This is because Windows strips details from your prompt which are required for inshellisense to work. To do this, update your config file in your home directory and add the following configuration:
112
-
113
- ```toml
114
- [[prompt.bash]]
115
- regex = "(?<prompt>^>\\s*)" # the prompt match group will be used to detect the prompt
116
- postfix = ">" # the postfix is the last expected character in your prompt
117
- ```
118
-
119
- This example adds custom prompt detection for bash where the prompt is expected to be only `> `. You can add similar configurations for other shells as well as well as multiple configurations for each shell.
120
-
121
109
  ## Contributing
122
110
 
123
111
  This project welcomes contributions and suggestions. Most contributions require you to agree to a
@@ -4,19 +4,21 @@ import convert from "color-convert";
4
4
  import os from "node:os";
5
5
  import { getShellPromptRewrites, Shell } from "../utils/shell.js";
6
6
  import log from "../utils/log.js";
7
- import { getConfig } from "../utils/config.js";
8
7
  const maxPromptPollDistance = 10;
9
8
  export class CommandManager {
10
9
  #activeCommand;
11
10
  #terminal;
12
11
  #previousCommandLines;
12
+ #maxCursorY;
13
13
  #shell;
14
14
  #promptRewrites;
15
15
  #supportsProperOscPlacements = os.platform() !== "win32";
16
+ promptTerminator = "";
16
17
  constructor(terminal, shell) {
17
18
  this.#terminal = terminal;
18
19
  this.#shell = shell;
19
20
  this.#activeCommand = {};
21
+ this.#maxCursorY = 0;
20
22
  this.#previousCommandLines = new Set();
21
23
  this.#promptRewrites = getShellPromptRewrites(shell);
22
24
  if (this.#supportsProperOscPlacements) {
@@ -45,17 +47,9 @@ export class CommandManager {
45
47
  }
46
48
  handleClear() {
47
49
  this.handlePromptStart();
50
+ this.#maxCursorY = 0;
48
51
  this.#previousCommandLines = new Set();
49
52
  }
50
- _extractPrompt(lineText, patterns) {
51
- for (const { regex, postfix } of patterns) {
52
- const customPrompt = lineText.match(new RegExp(regex))?.groups?.prompt;
53
- const adjustedPrompt = this._adjustPrompt(customPrompt, lineText, postfix);
54
- if (adjustedPrompt) {
55
- return adjustedPrompt;
56
- }
57
- }
58
- }
59
53
  _getWindowsPrompt(y) {
60
54
  const line = this.#terminal.buffer.active.getLine(y);
61
55
  if (!line) {
@@ -65,15 +59,16 @@ export class CommandManager {
65
59
  if (!lineText) {
66
60
  return;
67
61
  }
62
+ // dynamic prompt terminator
63
+ if (this.promptTerminator && lineText.trim().endsWith(this.promptTerminator)) {
64
+ const adjustedPrompt = this._adjustPrompt(lineText, lineText, this.promptTerminator);
65
+ if (adjustedPrompt) {
66
+ return adjustedPrompt;
67
+ }
68
+ }
68
69
  // User defined prompt
69
- const inshellisenseConfig = getConfig();
70
70
  if (this.#shell == Shell.Bash) {
71
- if (inshellisenseConfig?.prompt?.bash != null) {
72
- const extractedPrompt = this._extractPrompt(lineText, inshellisenseConfig.prompt.bash);
73
- if (extractedPrompt)
74
- return extractedPrompt;
75
- }
76
- const bashPrompt = lineText.match(/^(?<prompt>.*\$\s?)/)?.groups?.prompt;
71
+ const bashPrompt = lineText.match(/^(?<prompt>\$\s?)/)?.groups?.prompt;
77
72
  if (bashPrompt) {
78
73
  const adjustedPrompt = this._adjustPrompt(bashPrompt, lineText, "$");
79
74
  if (adjustedPrompt) {
@@ -81,12 +76,16 @@ export class CommandManager {
81
76
  }
82
77
  }
83
78
  }
84
- if (this.#shell == Shell.Nushell) {
85
- if (inshellisenseConfig?.prompt?.nu != null) {
86
- const extractedPrompt = this._extractPrompt(lineText, inshellisenseConfig.prompt.nu);
87
- if (extractedPrompt)
88
- return extractedPrompt;
79
+ if (this.#shell == Shell.Fish) {
80
+ const fishPrompt = lineText.match(/(?<prompt>.*>\s?)/)?.groups?.prompt;
81
+ if (fishPrompt) {
82
+ const adjustedPrompt = this._adjustPrompt(fishPrompt, lineText, ">");
83
+ if (adjustedPrompt) {
84
+ return adjustedPrompt;
85
+ }
89
86
  }
87
+ }
88
+ if (this.#shell == Shell.Nushell) {
90
89
  const nushellPrompt = lineText.match(/(?<prompt>.*>\s?)/)?.groups?.prompt;
91
90
  if (nushellPrompt) {
92
91
  const adjustedPrompt = this._adjustPrompt(nushellPrompt, lineText, ">");
@@ -96,11 +95,6 @@ export class CommandManager {
96
95
  }
97
96
  }
98
97
  if (this.#shell == Shell.Xonsh) {
99
- if (inshellisenseConfig?.prompt?.xonsh != null) {
100
- const extractedPrompt = this._extractPrompt(lineText, inshellisenseConfig.prompt.xonsh);
101
- if (extractedPrompt)
102
- return extractedPrompt;
103
- }
104
98
  let xonshPrompt = lineText.match(/(?<prompt>.*@\s?)/)?.groups?.prompt;
105
99
  if (xonshPrompt) {
106
100
  const adjustedPrompt = this._adjustPrompt(xonshPrompt, lineText, "@");
@@ -117,16 +111,6 @@ export class CommandManager {
117
111
  }
118
112
  }
119
113
  if (this.#shell == Shell.Powershell || this.#shell == Shell.Pwsh) {
120
- if (inshellisenseConfig?.prompt?.powershell != null) {
121
- const extractedPrompt = this._extractPrompt(lineText, inshellisenseConfig.prompt.powershell);
122
- if (extractedPrompt)
123
- return extractedPrompt;
124
- }
125
- if (inshellisenseConfig?.prompt?.pwsh != null) {
126
- const extractedPrompt = this._extractPrompt(lineText, inshellisenseConfig.prompt.pwsh);
127
- if (extractedPrompt)
128
- return extractedPrompt;
129
- }
130
114
  const pwshPrompt = lineText.match(/(?<prompt>(\(.+\)\s)?(?:PS.+>\s?))/)?.groups?.prompt;
131
115
  if (pwshPrompt) {
132
116
  const adjustedPrompt = this._adjustPrompt(pwshPrompt, lineText, ">");
@@ -191,13 +175,78 @@ export class CommandManager {
191
175
  clearActiveCommand() {
192
176
  this.#activeCommand = {};
193
177
  }
178
+ _getCommandLines() {
179
+ const lines = [];
180
+ let lineY = this.#activeCommand.promptEndMarker.line;
181
+ let line = this.#terminal.buffer.active.getLine(this.#activeCommand.promptEndMarker.line);
182
+ const absoluteY = this.#terminal.buffer.active.baseY + this.#terminal.buffer.active.cursorY;
183
+ for (; lineY < this.#terminal.buffer.active.baseY + this.#terminal.rows;) {
184
+ if (line)
185
+ lines.push(line);
186
+ lineY += 1;
187
+ line = this.#terminal.buffer.active.getLine(lineY);
188
+ const lineWrapped = line?.isWrapped;
189
+ const cursorWrapped = absoluteY > lineY - 1;
190
+ const wrapped = lineWrapped || cursorWrapped;
191
+ if (!wrapped)
192
+ break;
193
+ }
194
+ return lines;
195
+ }
196
+ _getCommandText(commandLines) {
197
+ const absoluteY = this.#terminal.buffer.active.baseY + this.#terminal.buffer.active.cursorY;
198
+ const cursorLine = Math.max(absoluteY - this.#activeCommand.promptEndMarker.line, 0);
199
+ let preCursorCommand = "";
200
+ let postCursorCommand = "";
201
+ let suggestion = "";
202
+ for (const [y, line] of commandLines.entries()) {
203
+ const startX = y == 0 ? this.#activeCommand.promptText?.length ?? 0 : 0;
204
+ for (let x = startX; x < this.#terminal.cols; x++) {
205
+ if (postCursorCommand.endsWith(" "))
206
+ break; // assume that a command that ends with 4 spaces is terminated, avoids capturing right prompts
207
+ const cell = line.getCell(x);
208
+ if (cell == null)
209
+ continue;
210
+ const chars = cell.getChars() == "" ? " " : cell.getChars();
211
+ const beforeCursor = y < cursorLine || (y == cursorLine && x < this.#terminal.buffer.active.cursorX);
212
+ const isCommand = !this._isSuggestion(cell) && suggestion.length == 0;
213
+ if (isCommand && beforeCursor) {
214
+ preCursorCommand += chars;
215
+ }
216
+ else if (isCommand) {
217
+ postCursorCommand += chars;
218
+ }
219
+ else {
220
+ suggestion += chars;
221
+ }
222
+ }
223
+ }
224
+ log.debug({ msg: "command text", preCursorCommand, postCursorCommand, suggestion });
225
+ return { suggestion, preCursorCommand, postCursorCommand };
226
+ }
227
+ _getCommandOutputStatus(commandLines) {
228
+ const outputLineY = this.#activeCommand.promptEndMarker.line + commandLines;
229
+ const maxLineY = this.#terminal.buffer.active.baseY + this.#terminal.rows;
230
+ if (outputLineY >= maxLineY)
231
+ return false;
232
+ const line = this.#terminal.buffer.active.getLine(outputLineY);
233
+ let cell = undefined;
234
+ for (let i = 0; i < this.#terminal.cols; i++) {
235
+ cell = line?.getCell(i, cell);
236
+ if (cell?.getChars() != "") {
237
+ return true;
238
+ }
239
+ }
240
+ return false;
241
+ }
194
242
  termSync() {
195
243
  if (this.#activeCommand.promptEndMarker == null || this.#activeCommand.promptStartMarker == null) {
196
244
  return;
197
245
  }
198
246
  const globalCursorPosition = this.#terminal.buffer.active.baseY + this.#terminal.buffer.active.cursorY;
199
247
  const withinPollDistance = globalCursorPosition < this.#activeCommand.promptEndMarker.line + 5;
200
- if (globalCursorPosition < this.#activeCommand.promptStartMarker.line) {
248
+ this.#maxCursorY = Math.max(this.#maxCursorY, globalCursorPosition);
249
+ if (globalCursorPosition < this.#activeCommand.promptStartMarker.line || globalCursorPosition < this.#maxCursorY) {
201
250
  this.handleClear();
202
251
  this.#activeCommand.promptEndMarker = this.#terminal.registerMarker(0);
203
252
  return;
@@ -221,60 +270,15 @@ export class CommandManager {
221
270
  }
222
271
  // if the prompt is set, now parse out the values from the terminal
223
272
  if (this.#activeCommand.promptText != null) {
224
- let lineY = this.#activeCommand.promptEndMarker.line;
225
- let line = this.#terminal.buffer.active.getLine(this.#activeCommand.promptEndMarker.line);
226
- let command = "";
227
- let wrappedCommand = "";
228
- let suggestions = "";
229
- let isWrapped = false;
230
- for (; lineY < this.#terminal.buffer.active.baseY + this.#terminal.rows;) {
231
- for (let i = lineY == this.#activeCommand.promptEndMarker.line ? this.#activeCommand.promptText.length : 0; i < this.#terminal.cols; i++) {
232
- if (command.endsWith(" "))
233
- break; // assume that a command that ends with 4 spaces is terminated, avoids capturing right prompts
234
- const cell = line?.getCell(i);
235
- if (cell == null)
236
- continue;
237
- const chars = cell.getChars();
238
- const cleanedChars = chars == "" ? " " : chars;
239
- if (!this._isSuggestion(cell) && suggestions.length == 0) {
240
- command += cleanedChars;
241
- wrappedCommand += cleanedChars;
242
- }
243
- else {
244
- suggestions += cleanedChars;
245
- }
246
- }
247
- lineY += 1;
248
- line = this.#terminal.buffer.active.getLine(lineY);
249
- const wrapped = line?.isWrapped || this.#terminal.buffer.active.cursorY + this.#terminal.buffer.active.baseY != lineY - 1;
250
- isWrapped = isWrapped || wrapped;
251
- if (!wrapped) {
252
- break;
253
- }
254
- wrappedCommand = "";
255
- }
256
- const cursorAtEndOfInput = isWrapped
257
- ? wrappedCommand.trim().length % this.#terminal.cols <= this.#terminal.buffer.active.cursorX
258
- : (this.#activeCommand.promptText.length + command.trimEnd().length) % this.#terminal.cols <= this.#terminal.buffer.active.cursorX;
259
- let hasOutput = false;
260
- let cell = undefined;
261
- for (let i = 0; i < this.#terminal.cols; i++) {
262
- cell = line?.getCell(i, cell);
263
- if (cell == null)
264
- continue;
265
- hasOutput = cell.getChars() != "";
266
- if (hasOutput) {
267
- break;
268
- }
269
- }
270
- const postfixActive = isWrapped
271
- ? wrappedCommand.trim().length < this.#terminal.buffer.active.cursorX
272
- : this.#activeCommand.promptText.length + command.trimEnd().length < this.#terminal.buffer.active.cursorX;
273
- const commandPostfix = postfixActive ? " " : "";
273
+ const commandLines = this._getCommandLines();
274
+ const { suggestion, preCursorCommand, postCursorCommand } = this._getCommandText(commandLines);
275
+ const command = preCursorCommand + postCursorCommand.trim();
276
+ const cursorAtEndOfInput = postCursorCommand.trim() == "";
277
+ const hasOutput = this._getCommandOutputStatus(commandLines.length);
274
278
  this.#activeCommand.persistentOutput = this.#activeCommand.hasOutput && hasOutput;
275
279
  this.#activeCommand.hasOutput = hasOutput;
276
- this.#activeCommand.suggestionsText = suggestions.trim();
277
- this.#activeCommand.commandText = command.trim() + commandPostfix;
280
+ this.#activeCommand.suggestionsText = suggestion;
281
+ this.#activeCommand.commandText = command;
278
282
  this.#activeCommand.cursorTerminated = cursorAtEndOfInput;
279
283
  }
280
284
  log.debug({
@@ -282,6 +286,8 @@ export class CommandManager {
282
286
  ...this.#activeCommand,
283
287
  promptEndMarker: this.#activeCommand.promptEndMarker?.line,
284
288
  promptStartMarker: this.#activeCommand.promptStartMarker?.line,
289
+ cursorX: this.#terminal.buffer.active.cursorX,
290
+ cursorY: globalCursorPosition,
285
291
  });
286
292
  }
287
293
  }
@@ -6,6 +6,7 @@ import os from "node:os";
6
6
  import path from "node:path";
7
7
  import url from "node:url";
8
8
  import fs from "node:fs";
9
+ import stripAnsi from "strip-ansi";
9
10
  import pty from "@homebridge/node-pty-prebuilt-multiarch";
10
11
  import { Shell, userZdotdir, zdotdir } from "../utils/shell.js";
11
12
  import { IsTermOscPs, IstermOscPt, IstermPromptStart, IstermPromptEnd } from "../utils/ansi.js";
@@ -13,8 +14,8 @@ import xterm from "@xterm/headless";
13
14
  import { CommandManager } from "./commandManager.js";
14
15
  import log from "../utils/log.js";
15
16
  import { gitBashPath } from "../utils/shell.js";
16
- import ansi from "ansi-escapes";
17
17
  import styles from "ansi-styles";
18
+ import * as ansi from "../utils/ansi.js";
18
19
  const ISTermOnDataEvent = "data";
19
20
  export class ISTerm {
20
21
  pid;
@@ -84,6 +85,9 @@ export class ISTerm {
84
85
  }
85
86
  return cwd;
86
87
  }
88
+ _sanitizedPrompt(prompt) {
89
+ return stripAnsi(prompt);
90
+ }
87
91
  _handleIsSequence(data) {
88
92
  const argsIndex = data.indexOf(";");
89
93
  const sequence = argsIndex === -1 ? data : data.substring(0, argsIndex);
@@ -101,6 +105,19 @@ export class ISTerm {
101
105
  }
102
106
  break;
103
107
  }
108
+ case IstermOscPt.Prompt: {
109
+ const prompt = data.split(";").slice(1).join(";");
110
+ if (prompt != null) {
111
+ const sanitizedPrompt = this._sanitizedPrompt(this._deserializeIsMessage(prompt));
112
+ const lastPromptLine = sanitizedPrompt.substring(sanitizedPrompt.lastIndexOf("\n")).trim();
113
+ const promptTerminator = lastPromptLine.substring(lastPromptLine.lastIndexOf(" ")).trim();
114
+ if (promptTerminator) {
115
+ this.#commandManager.promptTerminator = promptTerminator;
116
+ log.debug({ msg: "prompt terminator", promptTerminator });
117
+ }
118
+ }
119
+ break;
120
+ }
104
121
  default:
105
122
  return false;
106
123
  }
@@ -216,7 +233,7 @@ export class ISTerm {
216
233
  const currentCursorPosition = this.#term.buffer.active.cursorY + this.#term.buffer.active.baseY;
217
234
  const writeLine = (y) => {
218
235
  const line = this.#term.buffer.active.getLine(y);
219
- const ansiLine = ["\x1b[0m"];
236
+ const ansiLine = [ansi.resetColor, ansi.resetLine];
220
237
  if (line == null)
221
238
  return "";
222
239
  let prevCell;
@@ -226,7 +243,7 @@ export class ISTerm {
226
243
  const sameColor = this._sameColor(prevCell, cell);
227
244
  const sameAccents = this._sameAccent(prevCell, cell);
228
245
  if (!sameColor || !sameAccents) {
229
- ansiLine.push("\x1b[0m");
246
+ ansiLine.push(ansi.resetColor);
230
247
  }
231
248
  if (!sameColor) {
232
249
  ansiLine.push(this._getAnsiColors(cell));
@@ -234,7 +251,7 @@ export class ISTerm {
234
251
  if (!sameAccents) {
235
252
  ansiLine.push(this._getAnsiAccents(cell));
236
253
  }
237
- ansiLine.push(chars == "" ? " " : chars);
254
+ ansiLine.push(chars == "" ? ansi.cursorForward() : chars);
238
255
  prevCell = cell;
239
256
  }
240
257
  return ansiLine.join("");
@@ -275,7 +292,10 @@ const convertToPtyTarget = async (shell, underTest, login) => {
275
292
  shellArgs = ["-noexit", "-command", `try { . "${path.join(shellFolderPath, "shellIntegration.ps1")}" } catch {}`];
276
293
  break;
277
294
  case Shell.Fish:
278
- shellArgs = ["--init-command", `. ${path.join(shellFolderPath, "shellIntegration.fish").replace(/(\s+)/g, "\\$1")}`];
295
+ shellArgs =
296
+ platform == "win32"
297
+ ? ["--init-command", `. "$(cygpath -u '${path.join(shellFolderPath, "shellIntegration.fish")}')"`]
298
+ : ["--init-command", `. ${path.join(shellFolderPath, "shellIntegration.fish").replace(/(\s+)/g, "\\$1")}`];
279
299
  break;
280
300
  case Shell.Xonsh: {
281
301
  const sharedConfig = os.platform() == "win32" ? path.join("C:\\ProgramData", "xonsh", "xonshrc") : path.join("etc", "xonsh", "xonshrc");
@@ -19,6 +19,7 @@ export class SuggestionManager {
19
19
  #activeSuggestionIdx;
20
20
  #suggestBlob;
21
21
  #shell;
22
+ #hideSuggestions = false;
22
23
  constructor(terminal, shell) {
23
24
  this.#term = terminal;
24
25
  this.#suggestBlob = { suggestions: [] };
@@ -28,7 +29,7 @@ export class SuggestionManager {
28
29
  }
29
30
  async _loadSuggestions() {
30
31
  const commandText = this.#term.getCommandState().commandText;
31
- if (!commandText) {
32
+ if (!commandText || this.#hideSuggestions) {
32
33
  this.#suggestBlob = undefined;
33
34
  this.#activeSuggestionIdx = 0;
34
35
  return;
@@ -125,12 +126,17 @@ export class SuggestionManager {
125
126
  if (name == "return") {
126
127
  this.#term.clearCommand(); // clear the current command on enter
127
128
  }
129
+ // if suggestions are hidden, keep them hidden until during command navigation
130
+ if (this.#hideSuggestions) {
131
+ this.#hideSuggestions = name == "up" || name == "down";
132
+ }
128
133
  if (!this.#suggestBlob) {
129
134
  return false;
130
135
  }
131
136
  const { dismissSuggestions: { key: dismissKey, shift: dismissShift, control: dismissCtrl }, acceptSuggestion: { key: acceptKey, shift: acceptShift, control: acceptCtrl }, nextSuggestion: { key: nextKey, shift: nextShift, control: nextCtrl }, previousSuggestion: { key: prevKey, shift: prevShift, control: prevCtrl }, } = getConfig().bindings;
132
137
  if (name == dismissKey && shift == !!dismissShift && ctrl == !!dismissCtrl) {
133
138
  this.#suggestBlob = undefined;
139
+ this.#hideSuggestions = true;
134
140
  }
135
141
  else if (name == prevKey && shift == !!prevShift && ctrl == !!prevCtrl) {
136
142
  this.#activeSuggestionIdx = Math.max(0, this.#activeSuggestionIdx - 1);
package/build/ui/utils.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
3
  import ansi from "ansi-escapes";
4
+ import { resetColor } from "../utils/ansi.js";
4
5
  import wrapAnsi from "wrap-ansi";
5
6
  import chalk from "chalk";
6
7
  import wcwidth from "wcwidth";
@@ -12,7 +13,7 @@ import wcwidth from "wcwidth";
12
13
  */
13
14
  export const renderBox = (rows, width, x, borderColor) => {
14
15
  const result = [];
15
- const setColor = (text) => (borderColor ? chalk.hex(borderColor).apply(text) : text);
16
+ const setColor = (text) => resetColor + (borderColor ? chalk.hex(borderColor).apply(text) : text);
16
17
  result.push(ansi.cursorTo(x) + setColor("┌" + "─".repeat(width - 2) + "┐") + ansi.cursorTo(x));
17
18
  rows.forEach((row) => {
18
19
  result.push(ansi.cursorDown() + setColor("│") + row + setColor("│") + ansi.cursorTo(x));
@@ -11,6 +11,7 @@ export var IstermOscPt;
11
11
  IstermOscPt["PromptStarted"] = "PS";
12
12
  IstermOscPt["PromptEnded"] = "PE";
13
13
  IstermOscPt["CurrentWorkingDirectory"] = "CWD";
14
+ IstermOscPt["Prompt"] = "PROMPT";
14
15
  })(IstermOscPt || (IstermOscPt = {}));
15
16
  export const IstermPromptStart = IS_OSC + IstermOscPt.PromptStarted + BEL;
16
17
  export const IstermPromptEnd = IS_OSC + IstermOscPt.PromptEnded + BEL;
@@ -18,7 +19,10 @@ export const cursorHide = CSI + "?25l";
18
19
  export const cursorShow = CSI + "?25h";
19
20
  export const cursorNextLine = CSI + "E";
20
21
  export const eraseLine = CSI + "2K";
22
+ export const resetColor = CSI + "0m";
23
+ export const resetLine = CSI + "2K";
21
24
  export const cursorBackward = (count = 1) => CSI + count + "D";
25
+ export const cursorForward = (count = 1) => CSI + count + "C";
22
26
  export const cursorTo = ({ x, y }) => {
23
27
  return CSI + (y ?? "") + ";" + (x ?? "") + "H";
24
28
  };
@@ -49,6 +49,7 @@ const configSchema = {
49
49
  acceptSuggestion: bindingSchema,
50
50
  },
51
51
  },
52
+ // DEPRECATED: prompt patterns are no longer used
52
53
  prompt: {
53
54
  type: "object",
54
55
  nullable: true,
@@ -58,6 +59,7 @@ const configSchema = {
58
59
  powershell: promptPatternsSchema,
59
60
  xonsh: promptPatternsSchema,
60
61
  nu: promptPatternsSchema,
62
+ fish: promptPatternsSchema,
61
63
  },
62
64
  },
63
65
  specs: {
@@ -70,9 +72,12 @@ const configSchema = {
70
72
  },
71
73
  additionalProperties: false,
72
74
  };
73
- const configFile = ".inshellisenserc";
75
+ const rcFile = ".inshellisenserc";
76
+ const xdgFile = "rc.toml";
74
77
  const cachePath = path.join(os.homedir(), ".inshellisense");
75
- const configPath = path.join(os.homedir(), configFile);
78
+ const rcPath = path.join(os.homedir(), rcFile);
79
+ const xdgPath = path.join(os.homedir(), ".config", "inshellisense", xdgFile);
80
+ const configPaths = [rcPath, xdgPath];
76
81
  let globalConfig = {
77
82
  bindings: {
78
83
  nextSuggestion: { key: "down" },
@@ -83,38 +88,42 @@ let globalConfig = {
83
88
  };
84
89
  export const getConfig = () => globalConfig;
85
90
  export const loadConfig = async (program) => {
86
- if (fs.existsSync(configPath)) {
87
- let config;
88
- try {
89
- config = toml.parse((await fsAsync.readFile(configPath)).toString());
90
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
91
+ configPaths.forEach(async (configPath) => {
92
+ if (fs.existsSync(configPath)) {
93
+ let config;
94
+ try {
95
+ config = toml.parse((await fsAsync.readFile(configPath)).toString());
96
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
97
+ }
98
+ catch (e) {
99
+ program.error(`${configPath} is invalid toml. Parsing error on line ${e.line}, column ${e.column}: ${e.message}`);
100
+ }
101
+ const isValid = ajv.validate(configSchema, config);
102
+ if (!isValid) {
103
+ program.error(`${configPath} is invalid: ${ajv.errorsText()}`);
104
+ }
105
+ globalConfig = {
106
+ bindings: {
107
+ nextSuggestion: config?.bindings?.nextSuggestion ?? globalConfig.bindings.nextSuggestion,
108
+ previousSuggestion: config?.bindings?.previousSuggestion ?? globalConfig.bindings.previousSuggestion,
109
+ acceptSuggestion: config?.bindings?.acceptSuggestion ?? globalConfig.bindings.acceptSuggestion,
110
+ dismissSuggestions: config?.bindings?.dismissSuggestions ?? globalConfig.bindings.dismissSuggestions,
111
+ },
112
+ prompt: {
113
+ bash: config.prompt?.bash ?? globalConfig?.prompt?.bash,
114
+ powershell: config.prompt?.powershell ?? globalConfig?.prompt?.powershell,
115
+ xonsh: config.prompt?.xonsh ?? globalConfig?.prompt?.xonsh,
116
+ pwsh: config.prompt?.pwsh ?? globalConfig?.prompt?.pwsh,
117
+ nu: config.prompt?.nu ?? globalConfig?.prompt?.nu,
118
+ fish: config.prompt?.fish ?? globalConfig?.prompt?.fish,
119
+ },
120
+ specs: {
121
+ path: [...(config?.specs?.path ?? []), ...(config?.specs?.path ?? [])],
122
+ },
123
+ };
91
124
  }
92
- catch (e) {
93
- program.error(`${configFile} is invalid toml. Parsing error on line ${e.line}, column ${e.column}: ${e.message}`);
94
- }
95
- const isValid = ajv.validate(configSchema, config);
96
- if (!isValid) {
97
- program.error(`${configFile} is invalid: ${ajv.errorsText()}`);
98
- }
99
- globalConfig = {
100
- bindings: {
101
- nextSuggestion: config?.bindings?.nextSuggestion ?? globalConfig.bindings.nextSuggestion,
102
- previousSuggestion: config?.bindings?.previousSuggestion ?? globalConfig.bindings.previousSuggestion,
103
- acceptSuggestion: config?.bindings?.acceptSuggestion ?? globalConfig.bindings.acceptSuggestion,
104
- dismissSuggestions: config?.bindings?.dismissSuggestions ?? globalConfig.bindings.dismissSuggestions,
105
- },
106
- prompt: {
107
- bash: config.prompt?.bash,
108
- powershell: config.prompt?.powershell,
109
- xonsh: config.prompt?.xonsh,
110
- pwsh: config.prompt?.pwsh,
111
- nu: config.prompt?.nu,
112
- },
113
- specs: {
114
- path: [`${os.homedir()}/.fig/autocomplete/build`, ...(config?.specs?.path ?? [])],
115
- },
116
- };
117
- }
125
+ });
126
+ globalConfig.specs = { path: [`${os.homedir()}/.fig/autocomplete/build`, ...(globalConfig.specs?.path ?? [])] };
118
127
  };
119
128
  export const deleteCacheFolder = async () => {
120
129
  const cliConfigPath = path.join(os.homedir(), cachePath);
@@ -50,6 +50,23 @@ export const setupZshDotfiles = async () => {
50
50
  await fsAsync.cp(path.join(shellFolderPath, "shellIntegration-login.zsh"), path.join(zdotdir, ".zlogin"));
51
51
  };
52
52
  export const inferShell = async () => {
53
+ // try getting shell from shell specific env variables
54
+ if (process.env.NU_VERSION != null) {
55
+ return Shell.Nushell;
56
+ }
57
+ else if (process.env.XONSHRC != null) {
58
+ return Shell.Xonsh;
59
+ }
60
+ else if (process.env.FISH_VERSION != null) {
61
+ return Shell.Fish;
62
+ }
63
+ else if (process.env.ZSH_VERSION != null) {
64
+ return Shell.Zsh;
65
+ }
66
+ else if (process.env.BASH_VERSION != null) {
67
+ return Shell.Bash;
68
+ }
69
+ // try getting shell from env
53
70
  try {
54
71
  const name = path.parse(process.env.SHELL ?? "").name;
55
72
  const shellName = supportedShells.find((shell) => name.includes(shell));
@@ -59,6 +76,7 @@ export const inferShell = async () => {
59
76
  catch {
60
77
  /* empty */
61
78
  }
79
+ // try getting shell from parent process
62
80
  const processResult = (await find("pid", process.ppid)).at(0);
63
81
  const name = processResult?.name;
64
82
  return name != null ? supportedShells.find((shell) => name.includes(shell)) : undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microsoft/inshellisense",
3
- "version": "0.0.1-rc.14",
3
+ "version": "0.0.1-rc.15",
4
4
  "description": "IDE style command line auto complete",
5
5
  "type": "module",
6
6
  "engines": {
@@ -49,6 +49,7 @@
49
49
  "color-convert": "^2.0.1",
50
50
  "commander": "^11.0.0",
51
51
  "find-process": "^1.4.7",
52
+ "strip-ansi": "^7.1.0",
52
53
  "toml": "^3.0.0",
53
54
  "wcwidth": "^1.0.1",
54
55
  "which": "^4.0.0",
@@ -40,6 +40,10 @@ __is_escape_value() {
40
40
  token="\\\\"
41
41
  elif [ "$byte" = ";" ]; then
42
42
  token="\\x3b"
43
+ elif [ "$byte" = $'\n' ]; then
44
+ token="\x0a"
45
+ elif [ "$byte" = $'\e' ]; then
46
+ token="\\x1b"
43
47
  else
44
48
  token="$byte"
45
49
  fi
@@ -54,6 +58,16 @@ __is_update_cwd() {
54
58
  builtin printf '\e]6973;CWD;%s\a' "$(__is_escape_value "$PWD")"
55
59
  }
56
60
 
61
+ __is_report_prompt() {
62
+ if ((BASH_VERSINFO[0] >= 4)); then
63
+ __is_prompt=${__is_original_PS1@P}
64
+ else
65
+ __is_prompt=${__is_original_PS1}
66
+ fi
67
+ __is_prompt="$(builtin printf "%s" "${__is_prompt//[$'\001'$'\002']}")"
68
+ builtin printf "\e]6973;PROMPT;%s\a" "$(__is_escape_value "${__is_prompt}")"
69
+ }
70
+
57
71
  if [[ -n "${bash_preexec_imported:-}" ]]; then
58
72
  precmd_functions+=(__is_precmd)
59
73
  fi
@@ -61,6 +75,7 @@ fi
61
75
  __is_precmd() {
62
76
  __is_update_cwd
63
77
  __is_update_prompt
78
+ __is_report_prompt
64
79
  }
65
80
 
66
81
  __is_update_prompt() {
@@ -2,15 +2,18 @@ function __is_copy_function; functions $argv[1] | sed "s/^function $argv[1]/func
2
2
  function __is_prompt_start; printf '\e]6973;PS\a'; end
3
3
  function __is_prompt_end; printf '\e]6973;PE\a'; end
4
4
 
5
+ __is_copy_function fish_prompt is_user_prompt
6
+
5
7
  function __is_escape_value
6
8
  echo $argv \
7
- | string replace --all '\\' '\\\\' \
8
- | string replace --all ';' '\\x3b' \
9
+ | string replace -a '\\' '\\\\' \
10
+ | string replace -a ';' '\\x3b' \
11
+ | string replace -a \e '\\x1b' \
12
+ | string split \n | string join '\x0a' \
9
13
  ;
10
14
  end
11
- function __is_update_cwd --on-event fish_prompt; set __is_cwd (__is_escape_value "$PWD"); printf "\e]6973;CWD;$__is_cwd\a"; end
12
-
13
- __is_copy_function fish_prompt is_user_prompt
15
+ function __is_update_cwd --on-event fish_prompt; set __is_cwd (__is_escape_value "$PWD"); printf "\e]6973;CWD;%s\a" $__is_cwd; end
16
+ function __is_report_prompt --on-event fish_prompt; set __is_prompt (__is_escape_value (is_user_prompt)); printf "\e]6973;PROMPT;%s\a" $__is_prompt; end
14
17
 
15
18
  if [ "$ISTERM_TESTING" = "1" ]
16
19
  function is_user_prompt; printf '> '; end
@@ -1,18 +1,26 @@
1
- let __is_escape_value = {|x| $x | str replace --all "\\" "\\\\" | str replace --all ";" "\\x3b" }
1
+ let __is_escape_value = {|x| $x | str replace --all "\\" "\\\\" | str replace --all ";" "\\x3b" | str replace --all "\n" '\x0a' | str replace --all "\e" "\\x1b" }
2
+ let __is_original_PROMPT_COMMAND = if 'PROMPT_COMMAND' in $env { $env.PROMPT_COMMAND } else { "" }
3
+ let __is_original_PROMPT_INDICATOR = if 'PROMPT_INDICATOR' in $env { $env.PROMPT_INDICATOR } else { "" }
2
4
 
3
5
  let __is_update_cwd = {
4
6
  let pwd = do $__is_escape_value $env.PWD
5
7
  $"\e]6973;CWD;($pwd)\a"
6
8
  }
7
- let __is_original_PROMPT_COMMAND = if 'PROMPT_COMMAND' in $env { $env.PROMPT_COMMAND } else { "" }
9
+ let __is_report_prompt = {
10
+ let __is_indicatorCommandType = $__is_original_PROMPT_INDICATOR | describe
11
+ mut __is_prompt_ind = if $__is_indicatorCommandType == "closure" { do $__is_original_PROMPT_INDICATOR } else { $__is_original_PROMPT_INDICATOR }
12
+ let __is_esc_prompt_ind = do $__is_escape_value $__is_prompt_ind
13
+ $"\e]6973;PROMPT;($__is_esc_prompt_ind)\a"
14
+ }
8
15
  let __is_custom_PROMPT_COMMAND = {
9
16
  let promptCommandType = $__is_original_PROMPT_COMMAND | describe
10
17
  mut cmd = if $promptCommandType == "closure" { do $__is_original_PROMPT_COMMAND } else { $__is_original_PROMPT_COMMAND }
11
18
  let pwd = do $__is_update_cwd
19
+ let prompt = do $__is_report_prompt
12
20
  if 'ISTERM_TESTING' in $env {
13
21
  $cmd = ""
14
22
  }
15
- $"\e]6973;PS\a($cmd)($pwd)"
23
+ $"\e]6973;PS\a($cmd)($pwd)($prompt)"
16
24
  }
17
25
  $env.PROMPT_COMMAND = $__is_custom_PROMPT_COMMAND
18
26
 
@@ -8,7 +8,7 @@ if ($env:ISTERM_TESTING -eq "1") {
8
8
  }
9
9
 
10
10
  function Global:__IS-Escape-Value([string]$value) {
11
- [regex]::Replace($value, '[\\\n;]', { param($match)
11
+ [regex]::Replace($value, "[$([char]0x1b)\\\n;]", { param($match)
12
12
  -Join (
13
13
  [System.Text.Encoding]::UTF8.GetBytes($match.Value) | ForEach-Object { '\x{0:x2}' -f $_ }
14
14
  )
@@ -17,8 +17,11 @@ function Global:__IS-Escape-Value([string]$value) {
17
17
 
18
18
  function Global:Prompt() {
19
19
  $Result = "$([char]0x1b)]6973;PS`a"
20
- $Result += $Global:__IsOriginalPrompt.Invoke()
20
+ $OriginalPrompt += $Global:__IsOriginalPrompt.Invoke()
21
+ $Result += $OriginalPrompt
21
22
  $Result += "$([char]0x1b)]6973;PE`a"
23
+
24
+ $Result += "$([char]0x1b)]6973;PROMPT;$(__IS-Escape-Value $OriginalPrompt)`a"
22
25
  $Result += if ($pwd.Provider.Name -eq 'FileSystem') { "$([char]0x1b)]6973;CWD;$(__IS-Escape-Value $pwd.ProviderPath)`a" }
23
26
  return $Result
24
27
  }
@@ -1,4 +1,5 @@
1
1
  import os
2
+ from xonsh.main import XSH
2
3
 
3
4
  def __is_prompt_start() -> str:
4
5
  return "\001" + "\x1b]6973;PS\x07"
@@ -7,23 +8,30 @@ def __is_prompt_start() -> str:
7
8
  def __is_prompt_end() -> str:
8
9
  return "\001" + "\x1b]6973;PE\x07" + "\002"
9
10
 
10
-
11
11
  def __is_escape_value(value: str) -> str:
12
12
  byte_list = [bytes([byte]).decode("utf-8") for byte in list(value.encode("utf-8"))]
13
13
  return "".join(
14
14
  [
15
- "\\x3b" if byte == ";" else "\\\\" if byte == "\\" else byte
15
+ "\\x3b" if byte == ";" else "\\\\" if byte == "\\" else "\\x1b" if byte == "\x1b" else "\x0a" if byte == "\n" else byte
16
16
  for byte in byte_list
17
17
  ]
18
18
  )
19
19
 
20
20
  def __is_update_cwd() -> str:
21
- return f"\x1b]6973;CWD;{__is_escape_value(os.getcwd())}\x07" + "\002"
21
+ return f"\x1b]6973;CWD;{__is_escape_value(os.getcwd())}\x07"
22
+
23
+ __is_original_prompt = $PROMPT
24
+ def __is_report_prompt() -> str:
25
+ prompt = ""
26
+ formatted_prompt = XSH.shell.prompt_formatter(__is_original_prompt)
27
+ prompt = "".join([text for _, text in XSH.shell.format_color(formatted_prompt)])
28
+ return f"\x1b]6973;PROMPT;{__is_escape_value(prompt)}\x07" + "\002"
22
29
 
23
30
  $PROMPT_FIELDS['__is_prompt_start'] = __is_prompt_start
24
31
  $PROMPT_FIELDS['__is_prompt_end'] = __is_prompt_end
25
32
  $PROMPT_FIELDS['__is_update_cwd'] = __is_update_cwd
33
+ $PROMPT_FIELDS['__is_report_prompt'] = __is_report_prompt
26
34
  if 'ISTERM_TESTING' in ${...}:
27
35
  $PROMPT = "> "
28
36
 
29
- $PROMPT = "{__is_prompt_start}{__is_update_cwd}" + $PROMPT + "{__is_prompt_end}"
37
+ $PROMPT = "{__is_prompt_start}{__is_update_cwd}{__is_report_prompt}" + $PROMPT + "{__is_prompt_end}"