fish-lsp
Version:
LSP implementation for fish/fish-shell
204 lines (176 loc) • 6.27 kB
text/typescript
import { Connection } from 'vscode-languageserver';
import { exec } from 'child_process';
import { execAsyncFish } from './utils/exec';
import { promisify } from 'util';
import { appendFileSync } from 'fs';
export const execAsync = promisify(exec);
export type ExecResultKind = 'error' | 'info';
export type ExecResultWrapper = {
message: string;
kind: ExecResultKind;
};
export async function execLineInBuffer(line: string): Promise<ExecResultWrapper> {
const { stderr, stdout } = await execAsync(`fish -c '${line}'; or true`);
if (stderr) {
return { message: buildOutput(line, 'stderr:', stderr), kind: 'error' };
}
if (stdout) {
return { message: buildOutput(line, 'stdout:', stdout), kind: 'info' };
}
return {
message: [
`${fishLspPromptIcon} ${line}`,
'-'.repeat(50),
'EMPTY RESULT',
].join('\n'),
kind: 'info',
};
}
export const fishLspPromptIcon = '><(((°>';
export function buildOutput(line: string, outputMessage: 'error:' | 'stderr:' | 'stdout:', output: string) {
const tokens = line.trim().split(' ');
let promptLine = `${fishLspPromptIcon} `;
let currentLen = promptLine.length;
for (const token of tokens) {
if (1 + token.length + currentLen > 49) {
const newToken = `\\\n ${token} `;
promptLine += newToken;
currentLen = newToken.slice(newToken.indexOf('\n')).length;
} else {
const newToken = token + ' ';
promptLine += newToken;
currentLen += newToken.length + 1;
}
}
return [
promptLine,
'-'.repeat(50),
`${outputMessage} ${output}`,
].join('\n');
}
export function buildExecuteNotificationResponse(
input: string,
output: { stdout: string; stderr: string; },
) {
const outputType = output.stdout ? output.stdout : output.stderr;
const outputMessagePrefix = output.stdout ? 'stdout:' : 'stderr:';
const kind: ExecResultKind = output.stdout ? 'info' : 'error';
return {
message: buildOutput(input, outputMessagePrefix, outputType),
kind,
};
}
export async function execEntireBuffer(bufferName: string): Promise<ExecResultWrapper> {
const { stdout, stderr } = await execAsync(`fish ${bufferName}`);
const statusOutput = (await execAsync(`fish -c 'fish ${bufferName} 1> /dev/null; echo "\\$status: $status"'`)).stdout;
const headerOutput = [
`${fishLspPromptIcon} executing file:`,
`${' '.repeat(fishLspPromptIcon.length)} ${bufferName}`,
].join('\n');
const longestLineLen = findLongestLine(headerOutput, stdout, stderr, '-'.repeat(50)).length;
let output = '';
if (stdout) output += `${stdout}`;
if (stdout && stderr) output += `\nerror:\n${stderr}`;
else if (!stdout && stderr) output += `error:\n${stderr}`;
let messageType: ExecResultKind = 'info';
if (stderr) messageType = 'error';
if (statusOutput) output += `${'-'.repeat(longestLineLen)}\n${statusOutput}`;
return {
message: [
headerOutput,
'-'.repeat(longestLineLen),
output,
].join('\n'),
kind: messageType,
};
}
export async function sourceFishBuffer(bufferName: string) {
const { stdout, stderr } = await execAsync(`fish -c 'source ${bufferName}'`);
const statusOutput = (await execAsync(`fish -c 'source ${bufferName} 1> /dev/null; echo "\\$status: $status"'`)).stdout;
const message = [
`${fishLspPromptIcon} sourcing file:`,
`${' '.repeat(fishLspPromptIcon.length)} ${bufferName}`,
].join('\n');
const longestLineLen = findLongestLine(message, stdout, stderr, statusOutput, '-'.repeat(50)).length;
const outputArr: string[] = [];
if (statusOutput) outputArr.push(statusOutput);
if (stdout) outputArr.push(stdout);
if (stderr) outputArr.push(stderr);
const output = outputArr.join('-'.repeat(50) + '\n');
return [
message,
'-'.repeat(longestLineLen),
output,
].join('\n');
}
export async function FishThemeDump() {
return (await execAsyncFish('fish_config theme dump; or true')).stdout.split('\n');
}
export async function showCurrentTheme(buffName: string) {
const output = (await execAsyncFish('fish_config theme demo; or true')).stdout.split('\n');
// Append the longest line to the file
for (const line of output) {
appendFileSync(buffName, `${line}\n`, 'utf8');
}
return {
message: `${fishLspPromptIcon} appended theme variables to end of file`,
kind: 'info',
};
}
export type ThemeOptions = {
asVariables: boolean;
};
const defaultThemeOptions: ThemeOptions = {
asVariables: false,
};
export async function executeThemeDump(buffName: string, options: ThemeOptions = defaultThemeOptions): Promise<ExecResultWrapper> {
const output = (await execAsyncFish('fish_config theme dump; or true')).stdout.split('\n');
// Append the longest line to the file
if (options.asVariables) {
appendFileSync(buffName, '# created by fish-lsp');
}
for (const line of output) {
if (options.asVariables) {
appendFileSync(buffName, `set -gx ${line}\n`, 'utf8');
} else {
appendFileSync(buffName, `${line}\n`, 'utf8');
}
}
return {
message: `${fishLspPromptIcon} appended theme variables to end of file`,
kind: 'info',
};
}
/**
* Function to find the longest line in a string.
* @param input - The input string with lines separated by newline characters.
* @returns The longest line in the input string.
*/
function findLongestLine(...inputs: string[]): string {
const input = inputs.join('\n');
// Split the input string by newline characters into an array of lines
const lines: string[] = input.split('\n');
// Initialize a variable to keep track of the longest line
let longestLine: string = '';
// Iterate over each line
for (const line of lines) {
// If the current line is longer than the longestLine found so far, update longestLine
if (line.length > longestLine.length) {
longestLine = line;
}
}
// Return the longest line found
return longestLine;
}
export function useMessageKind(connection: Connection, result: ExecResultWrapper) {
switch (result.kind) {
case 'info':
connection.window.showInformationMessage(result.message);
return;
case 'error':
connection.window.showErrorMessage(result.message);
return;
default:
return;
}
}