Skip to content
This repository has been archived by the owner on May 30, 2024. It is now read-only.

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
paracycle committed Jun 8, 2023
1 parent 92e3d9b commit c6cb2e7
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 38 deletions.
14 changes: 12 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,13 @@
},
{
"command": "bundler.remove",
"title": "Remove Gem",
"title": "Remove Dependency",
"icon": "$(trash)"
},
{
"command": "bundler.add",
"title": "Add Dependency",
"icon": "$(plus)"
}
],
"configuration": {
Expand Down Expand Up @@ -271,7 +276,12 @@
"view/item/context": [
{
"command": "bundler.remove",
"when": "view == bundler && viewItem == gem",
"when": "view == bundler && viewItem == dependency",
"group": "inline"
},
{
"command": "bundler.add",
"when": "view == bundler && viewItem == dependencies",
"group": "inline"
}
]
Expand Down
188 changes: 157 additions & 31 deletions src/bundler.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
import * as vscode from "vscode";

import { Ruby } from "./ruby";

Check failure on line 3 in src/bundler.ts

View workflow job for this annotation

GitHub Actions / build

'Ruby' is declared but its value is never read.
// eslint-disable-next-line import/no-cycle
import Client from "./client";

interface BundlerNode {
getChildren(): BundlerTreeNode[] | Thenable<BundlerTreeNode[]>;
getChildren(): BundlerTreeNode[] | undefined | Thenable<BundlerTreeNode[]>;
}

type BundlerTreeNode = Gem | GemDirectoryPath | GemFilePath;
type BundlerTreeNode =
| Dependencies
| Specs
| Spec
| GemDirectoryPath
| GemFilePath;

interface BundlerResponse {

Check failure on line 18 in src/bundler.ts

View workflow job for this annotation

GitHub Actions / build

'BundlerResponse' is declared but never used.
deps: [string];
specs: { [name: string]: { version: string; path: string } };
}
export class Bundler implements vscode.TreeDataProvider<BundlerTreeNode> {
private _onDidChangeTreeData: vscode.EventEmitter<any> =
new vscode.EventEmitter<any>();
Expand All @@ -16,13 +27,11 @@ export class Bundler implements vscode.TreeDataProvider<BundlerTreeNode> {
readonly onDidChangeTreeData: vscode.Event<any> =
this._onDidChangeTreeData.event;

constructor(private context: vscode.ExtensionContext, private ruby: Ruby) {
this.ruby = ruby;
this.context.subscriptions.push(
constructor(private client: Client) {
this.client.context.subscriptions.push(
vscode.window.createTreeView("bundler", { treeDataProvider: this }),
vscode.commands.registerCommand("bundler.remove", (gem) =>
this.removeGem(gem)
)
vscode.commands.registerCommand("bundler.remove", this.removeGem, this),
vscode.commands.registerCommand("bundler.add", this.addGem, this)
);
}

Expand All @@ -48,45 +57,146 @@ export class Bundler implements vscode.TreeDataProvider<BundlerTreeNode> {

private async runBundleCommand(command: string) {

Check failure on line 58 in src/bundler.ts

View workflow job for this annotation

GitHub Actions / build

'runBundleCommand' is declared but its value is never read.
return (
await this.ruby.run(`bundle ${command}`, { withOriginalGemfile: true })
await this.client.ruby.run(`bundle ${command}`, {
withOriginalGemfile: true,
})
)?.stdout;
}

private async fetchDependencies(): Promise<Gem[]> {
const versionList = await this.runBundleCommand("list");
private async fetchDependencies(): Promise<BundlerTreeNode[]> {
const resp = (await this.client.client?.sendRequest(
"ruby/dependencies",
{}
)) as {
dependencies: [{ name: string; version: string; path: string }];
gems: [{ name: string; version: string; path: string }];
};

if (!versionList) {
return [];
}
const dependencies = resp.dependencies.map((dep) => {
return new Dependency(dep.name, dep.version, vscode.Uri.file(dep.path));
});

const pathList = (await this.runBundleCommand("list --paths"))?.split("\n");
const gems = resp.gems.map((gem) => {
return new Spec(gem.name, gem.version, vscode.Uri.file(gem.path));
});

if (!pathList) {
return [];
}
return [new Dependencies(dependencies), new Specs(gems)];
// const response: BundlerResponse = JSON.parse(
// (await this.runBundleCommand(
// "exec ruby -rjson -e 'lock = Bundler.locked_gems; deps = lock.dependencies.keys; specs = lock.specs.map(&:materialize_for_installation).grep(Bundler::StubSpecification).sort_by(&:name).to_h { [_1.name, { version: _1.version, path: _1.full_gem_path }] }; puts JSON.dump({ deps: deps, specs: specs })'"
// )) || "{}"
// );

return Array.from(versionList.matchAll(/ {2}\* ([^(]+) \(([^)]+)\)/g)).map(
([_, name, version], idx) => {
return new Gem(name, version, vscode.Uri.file(pathList[idx]));
}
);
// if (!response || Object.keys(response).length === 0) {
// return [];
// }

// const deps: Dependency[] = response.deps
// .filter((name) => response.specs[name])
// .map((name) => {
// const { version, path } = response.specs[name];
// return new Dependency(name, version, vscode.Uri.file(path));
// });

// const specs = Object.entries(response.specs)
// .filter(([name]) => !response.deps.includes(name))
// .map(([name, { version, path }]) => {
// return new Spec(name, version, vscode.Uri.file(path));
// });

// return [new Dependencies(deps), new Specs(specs)];
}

private async removeGem(gem: Gem): Promise<void> {
private async removeGem(gem: Spec): Promise<void> {
const result = await vscode.window.showWarningMessage(
`Are you sure you want to remove the gem '${gem.name}' from the Gemfile?`,
{ title: "Yes" },
{ title: "No", isCloseAffordance: true }
);

if (result?.title === "Yes") {
await this.runBundleCommand(`remove ${gem.name}`);
await this.client.client?.sendNotification("ruby/dependencies/remove", {
name: gem.name,
gemfilePath: this.client.ruby.originalGemfile,
});

// await this.runBundleCommand(`remove ${gem.name}`);
this.refresh();
}
}

private async addGem(): Promise<void> {
const input = vscode.window.createQuickPick<GemPickItem>();
input.title = "Add Gem";
input.placeholder = "Search for a gem to add";

const gemName = await new Promise<string>((resolve) => {
input.onDidChangeValue(async (value) => {
input.busy = true;
const gems = await this.client.ruby.run(
`gem search --remote --no-verbose --no-versions '^${value}'`
);

if (gems?.stdout) {
input.items = gems.stdout
.split("\n")
.map((line) => new GemPickItem(line));
}
input.busy = false;
});

input.onDidChangeSelection(([gemName]) => {
resolve(gemName.label);
input.hide();
});

input.show();
});

if (gemName) {
await this.client.client?.sendNotification("ruby/dependencies/add", {
name: gemName,
gemfilePath: this.client.ruby.originalGemfile,
});
// await this.runBundleCommand(`add ${gemName}`);
this.refresh();
}
}
}

class Gem extends vscode.TreeItem implements BundlerNode {
class GemPickItem implements vscode.QuickPickItem {
label: string;

constructor(name: string) {
this.label = name;
}
}

class Dependencies extends vscode.TreeItem implements BundlerNode {
constructor(public readonly dependencies: Dependency[]) {
super("Dependencies", vscode.TreeItemCollapsibleState.Expanded);
this.contextValue = "dependencies";
this.iconPath = new vscode.ThemeIcon("package");
}

getChildren() {
return this.dependencies;
}
}

class Specs extends vscode.TreeItem implements BundlerNode {
constructor(public readonly specs: Spec[]) {
super("Specs", vscode.TreeItemCollapsibleState.Expanded);
this.contextValue = "specs";
this.iconPath = new vscode.ThemeIcon("package");
}

getChildren() {
return this.specs;
}
}

class Spec extends vscode.TreeItem implements BundlerNode {
constructor(
public readonly name: string,
public readonly version: string,
Expand All @@ -97,7 +207,7 @@ class Gem extends vscode.TreeItem implements BundlerNode {
this.iconPath = new vscode.ThemeIcon("ruby");
}

async getChildren(): Promise<BundlerTreeNode[]> {
async getChildren() {
const dir = this.resourceUri;
const entries = await vscode.workspace.fs.readDirectory(dir);

Expand All @@ -111,6 +221,13 @@ class Gem extends vscode.TreeItem implements BundlerNode {
}
}

class Dependency extends Spec {
constructor(name: string, version: string, resourceUri: vscode.Uri) {
super(name, version, resourceUri);
this.contextValue = "dependency";
}
}

class GemDirectoryPath extends vscode.TreeItem implements BundlerNode {
constructor(public readonly resourceUri: vscode.Uri) {
super(resourceUri, vscode.TreeItemCollapsibleState.Collapsed);
Expand All @@ -123,8 +240,17 @@ class GemDirectoryPath extends vscode.TreeItem implements BundlerNode {
};
}

getChildren(): BundlerTreeNode[] {
return [];
async getChildren() {
const dir = this.resourceUri;
const entries = await vscode.workspace.fs.readDirectory(dir);

return entries.map(([name, type]) => {
if (type === vscode.FileType.Directory) {
return new GemDirectoryPath(vscode.Uri.joinPath(dir, name));
} else {
return new GemFilePath(vscode.Uri.joinPath(dir, name));
}
});
}
}

Expand All @@ -141,7 +267,7 @@ class GemFilePath extends vscode.TreeItem implements BundlerNode {
};
}

getChildren(): BundlerTreeNode[] {
return [];
getChildren() {
return undefined;
}
}
18 changes: 15 additions & 3 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { Telemetry } from "./telemetry";
import { Ruby } from "./ruby";
import { StatusItems, Command, ServerState, ClientInterface } from "./status";
// eslint-disable-next-line import/no-cycle
import { Bundler } from "./bundler";

const LSP_NAME = "Ruby LSP";
Expand All @@ -26,7 +27,6 @@ interface EnabledFeatures {
}

export default class Client implements ClientInterface {
private client: LanguageClient | undefined;
private workingFolder: string;
private telemetry: Telemetry;
private statusItems: StatusItems;
Expand All @@ -37,6 +37,7 @@ export default class Client implements ClientInterface {

private terminal: vscode.Terminal | undefined;
private bundler: Bundler | undefined;
#client?: LanguageClient;
#context: vscode.ExtensionContext;
#ruby: Ruby;
#state: ServerState = ServerState.Starting;
Expand Down Expand Up @@ -109,7 +110,10 @@ export default class Client implements ClientInterface {

const configuration = vscode.workspace.getConfiguration("rubyLsp");
const clientOptions: LanguageClientOptions = {
documentSelector: [{ scheme: "file", language: "ruby" }],
documentSelector: [
{ scheme: "file", language: "ruby" },
{ scheme: "untitled", language: "ruby" },
],
diagnosticCollectionName: LSP_NAME,
outputChannel: this.outputChannel,
revealOutputChannelOn: RevealOutputChannelOn.Never,
Expand Down Expand Up @@ -222,6 +226,14 @@ export default class Client implements ClientInterface {
}
}

get client(): LanguageClient | undefined {
return this.#client;
}

private set client(client: LanguageClient | undefined) {
this.#client = client;
}

get ruby(): Ruby {
return this.#ruby;
}
Expand Down Expand Up @@ -273,7 +285,7 @@ export default class Client implements ClientInterface {
if (this.bundler) {
this.bundler.refresh();
} else {
this.bundler = new Bundler(this.context, this.ruby);
this.bundler = new Bundler(this);
}
}

Expand Down
6 changes: 5 additions & 1 deletion src/ruby.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export class Ruby {
return this._error;
}

get originalGemfile() {
return path.join(this.workingFolder, "Gemfile");
}

async activateRuby() {
this.versionManager = vscode.workspace
.getConfiguration("rubyLsp")
Expand Down Expand Up @@ -101,7 +105,7 @@ export class Ruby {

const env = { ...this._env };
if (options?.withOriginalGemfile) {
env.BUNDLE_GEMFILE = path.join(this.workingFolder, "Gemfile");
env.BUNDLE_GEMFILE = this.originalGemfile;
}
return asyncExec(command, { env });
}
Expand Down
1 change: 0 additions & 1 deletion src/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ export abstract class StatusItem {

constructor(id: string, client: ClientInterface) {
this.item = vscode.languages.createLanguageStatusItem(id, {
scheme: "file",
language: "ruby",
});
this.context = client.context;
Expand Down

0 comments on commit c6cb2e7

Please sign in to comment.