Skip to content

Commit

Permalink
feat: Add autocompletion for tool names and versions
Browse files Browse the repository at this point in the history
  • Loading branch information
hverlin committed Dec 23, 2024
1 parent 214c829 commit e742e6f
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 18 deletions.
12 changes: 11 additions & 1 deletion src/miseExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,20 @@ import {
import { createMenu } from "./extensionMenu";
import { MiseFileWatcher } from "./miseFileWatcher";
import { MiseService } from "./miseService";
import { ToolCompletionProvider } from "./providers/ToolCompletionProvider";
import {
MiseEnvsProvider,
registerEnvsCommands,
updateEnv,
} from "./providers/envProvider";
import { showToolVersionInline } from "./providers/inlineToolDecorator";
import { MiseCompletionProvider } from "./providers/miseCompletionProvider";
import { MiseFileTaskCodeLensProvider } from "./providers/miseFileTaskCodeLensProvider";
import {
TeraCompletionProvider,
createHoverProvider,
} from "./providers/miseTeraCompletionProvider";
import { MiseTomlCodeLensProvider } from "./providers/miseTomlCodeLensProvider";
import { showToolVersionInline } from "./providers/miseToolCompletionProvider";
import { registerTomlFileLinks } from "./providers/taskIncludesNavigation";
import {
MiseTasksProvider,
Expand Down Expand Up @@ -297,13 +298,22 @@ export class MiseExtension {
);
}

context.subscriptions.push(
vscode.languages.registerCompletionItemProvider(
allTomlFilesSelector,
new ToolCompletionProvider(this.miseService),
...['"', "'", "="],
),
);

context.subscriptions.push(
vscode.languages.registerCompletionItemProvider(
allTomlFilesSelector,
new TeraCompletionProvider(this.miseService),
...["{", "%", "|", "."],
),
);

context.subscriptions.push(
createHoverProvider(allTomlFilesSelector, this.miseService),
);
Expand Down
31 changes: 26 additions & 5 deletions src/miseService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ export class MiseService {
this.execMiseCommand(command, { setMiseEnv }),
);

private longCache = createCache({
ttl: 60,
storage: { type: "memory" },
}).define("execCmd", ({ command, setMiseEnv } = {}) =>
this.execMiseCommand(command, { setMiseEnv }),
);

async invalidateCache() {
await Promise.all([this.dedupeCache.clear(), this.cache.clear()]);
}
Expand Down Expand Up @@ -695,7 +702,8 @@ export class MiseService {
return [];
}

const { stdout } = await this.execMiseCommand("registry", {
const { stdout } = await this.longCache.execCmd({
command: "registry",
setMiseEnv: false,
});

Expand All @@ -714,6 +722,19 @@ export class MiseService {
);
}

async miseBackends() {
if (!this.getMiseBinaryPath()) {
return [];
}

const { stdout } = await this.longCache.execCmd({
command: "backends",
setMiseEnv: false,
});

return stdout.trim().split("\n");
}

async listRemoteVersions(
toolName: string,
{ yes = false } = {},
Expand All @@ -723,10 +744,10 @@ export class MiseService {
}

try {
const { stdout } = await this.execMiseCommand(
`ls-remote ${toolName}${yes ? " --yes" : ""}`,
{ setMiseEnv: false },
);
const { stdout } = await this.longCache.execCmd({
command: `ls-remote ${toolName}${yes ? " --yes" : ""}`,
setMiseEnv: false,
});
if (yes) {
return this.listRemoteVersions(toolName);
}
Expand Down
104 changes: 104 additions & 0 deletions src/providers/ToolCompletionProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import * as vscode from "vscode";
import type { MiseService } from "../miseService";
import { getCleanedToolName } from "../utils/miseUtilts";

export class ToolCompletionProvider implements vscode.CompletionItemProvider {
private miseService: MiseService;

constructor(miseService: MiseService) {
this.miseService = miseService;
}

async provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position,
) {
const [tools, backends] = await Promise.all([
this.miseService.miseRegistry(),
this.miseService.miseBackends(),
]);

const linePrefix = document
.lineAt(position)
.text.substring(0, position.character);

let inToolsSection = false;
for (let i = position.line; i >= 0; i--) {
const line = document.lineAt(i).text.trim();
if (line === "[tools]") {
inToolsSection = true;
break;
}
if (line.startsWith("[") && line !== "[tools]") {
break;
}
}

if (!inToolsSection) {
return [];
}

if (!linePrefix.includes("=")) {
const toolsCompletions = tools
.filter((tool) => tool.short !== undefined)
.map((tool) => {
const completionItem = new vscode.CompletionItem(
{
label: tool.short as string,
description: tool.full,
},
vscode.CompletionItemKind.Module,
);
completionItem.insertText = `${tool.short} = `;
completionItem.command = {
command: "editor.action.triggerSuggest",
title: "Re-trigger completions",
};
return completionItem;
});

const backendsCompletions = backends.map((backend) => {
const completionItem = new vscode.CompletionItem(
{ label: `'${backend}:`, description: `${backend} backend` },
vscode.CompletionItemKind.Value,
);
completionItem.insertText = `'${backend}:`;
return completionItem;
});

return toolsCompletions.concat(backendsCompletions);
}

const toolMatch = linePrefix.match(
/([a-z/'"-0-9:]*)\s*=\s*(["']?)([^"']*)$/,
);
if (!toolMatch) {
return [];
}

const [, toolName, existingQuote, partial] = toolMatch;
if (!toolName) {
return [];
}

const cleanedToolName = getCleanedToolName(toolName);
const versions = await this.miseService.listRemoteVersions(cleanedToolName);

return ["latest", ...versions]
.filter((version) => {
if (partial) {
return version.startsWith(partial.replace(/['"]/, ""));
}
return true;
})
.map((version, i) => {
const completionItem = new vscode.CompletionItem(
version,
vscode.CompletionItemKind.Value,
);
completionItem.sortText = i.toString();
completionItem.insertText = existingQuote ? version : `'${version}'`;
return completionItem;
});
}
}
24 changes: 20 additions & 4 deletions src/providers/envProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ export function registerEnvsCommands(
}

let previousEnvs: Map<string, string> = new Map();

function updateEnvironment(
context: vscode.ExtensionContext,
envs: Map<string, string>,
Expand All @@ -215,7 +216,14 @@ function updateEnvironment(

function updateTerminalsEnvs(variablesToRemove: [string, string][]) {
const commands = variablesToRemove.map(([name]) => `;unset ${name}`).join("");
const commandLine = `${commands};eval "$(mise env)"; clear;`;
const isTerminalFocused = vscode.window.activeTerminal !== undefined;

if (isTerminalFocused) {
return vscode.commands.executeCommand("workbench.action.terminal.relaunch");
}

const commandLine = ` ${commands}; eval "$(mise env)"; clear; # mise-vscode: env. updated. Run 'terminal: relaunch active terminal' to remove obsolete env variables.`;

for (const terminal of vscode.window.terminals) {
if (terminal.name.startsWith("mise-watch")) {
continue;
Expand Down Expand Up @@ -247,9 +255,17 @@ export async function updateEnv(
);
const shouldUpdateTerminal =
variablesToRemove.length > 0 ||
[...currentEnvs.entries()].some(
([name, value]) => previousEnvs.get(name) !== value,
);
[...currentEnvs.entries()].some(([name, value]) => {
if (!previousEnvs.has(name)) {
logger.info(`New env: ${name}=${value}`);
return true;
}
if (previousEnvs.get(name) !== value) {
logger.info(`Env changed: ${name}=${value}`);
return true;
}
return false;
});

updateEnvironment(context, currentEnvs, variablesToRemove);
if (shouldAutomaticallyReloadTerminalEnv() && shouldUpdateTerminal) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from "../configuration";
import type { MiseService } from "../miseService";
import { expandPath } from "../utils/fileUtils";
import { getCleanedToolName } from "../utils/miseUtilts";

const activeDecorationsPerFileAndTool: {
[filePath: string]: {
Expand Down Expand Up @@ -59,15 +60,11 @@ export async function showToolVersionInline(
}

const toolMatch = trimmedLine.match(/^([a-z/'"-0-9:]*)\s*=\s.*/);
if (!toolMatch) {
if (!toolMatch || !toolMatch[1]) {
continue;
}

const cleanedToolName = toolMatch[1]
?.trim()
.replace(/(['"])/g, "")
.replace("nodejs", "node")
.replace("golang", "go");
const cleanedToolName = getCleanedToolName(toolMatch[1]);
if (!cleanedToolName) {
continue;
}
Expand Down
4 changes: 2 additions & 2 deletions src/providers/toolsProvider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import * as os from "node:os";
import path from "node:path";
import * as vscode from "vscode";
import {
MISE_CONFIGURE_ALL_SKD_PATHS,
Expand All @@ -10,6 +8,7 @@ import {
MISE_INSTALL_TOOL,
MISE_OPEN_FILE,
MISE_OPEN_TOOL_DEFINITION,
MISE_RELOAD,
MISE_REMOVE_TOOL,
MISE_USE_TOOL,
} from "../commands";
Expand Down Expand Up @@ -339,6 +338,7 @@ export function registerToolsCommands(
),
vscode.commands.registerCommand(MISE_INSTALL_ALL, async () => {
await miseService.runMiseToolActionInConsole("install", "Install Tool");
await vscode.commands.executeCommand(MISE_RELOAD);
}),
vscode.commands.registerCommand(
MISE_USE_TOOL,
Expand Down
8 changes: 8 additions & 0 deletions src/utils/miseUtilts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,11 @@ export const idiomaticFileToTool = {
} as const;

export const idiomaticFiles = new Set(Object.keys(idiomaticFileToTool));

export const getCleanedToolName = (toolName: string) => {
return toolName
.trim()
.replace(/(['"])/g, "")
.replace("nodejs", "node")
.replace("golang", "go");
};

0 comments on commit e742e6f

Please sign in to comment.