Skip to content

Commit 9eb88e2

Browse files
authored
Merge pull request #549 from Shopify/andyw8/add-formatter-status-item
Add formatter status item
2 parents 29cfc97 + 58b9bd1 commit 9eb88e2

File tree

4 files changed

+96
-7
lines changed

4 files changed

+96
-7
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,13 @@ vscode.commands.registerCommand(
212212
);
213213
```
214214

215+
## Formatting
216+
217+
When `rubyLsp.formatter` is set to `auto`, Ruby LSP tries to determine which formatter to use.
218+
219+
If the bundle has a **direct** dependency on a supported formatter, such as `rubocop` or `syntax_tree`, that will be used.
220+
Otherwise, formatting will be disabled and you will need add one to the bundle.
221+
215222
## Contributing
216223

217224
Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/vscode-ruby-lsp.

src/client.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export default class Client implements ClientInterface {
4040
#context: vscode.ExtensionContext;
4141
#ruby: Ruby;
4242
#state: ServerState = ServerState.Starting;
43+
#formatter: string;
4344

4445
constructor(
4546
context: vscode.ExtensionContext,
@@ -52,6 +53,7 @@ export default class Client implements ClientInterface {
5253
this.testController = testController;
5354
this.#context = context;
5455
this.#ruby = ruby;
56+
this.#formatter = "";
5557
this.statusItems = new StatusItems(this);
5658
this.registerCommands();
5759
this.registerAutoRestarts();
@@ -207,6 +209,7 @@ export default class Client implements ClientInterface {
207209
);
208210

209211
await this.client.start();
212+
await this.determineFormatter();
210213

211214
this.state = ServerState.Running;
212215
}
@@ -261,6 +264,27 @@ export default class Client implements ClientInterface {
261264
this.#ruby = ruby;
262265
}
263266

267+
get formatter(): string {
268+
return this.#formatter;
269+
}
270+
271+
async determineFormatter() {
272+
const configuration = vscode.workspace.getConfiguration("rubyLsp");
273+
const configuredFormatter: string = configuration.get("formatter")!;
274+
275+
if (configuredFormatter === "auto") {
276+
if (await this.projectHasDependency(/^rubocop/)) {
277+
this.#formatter = "rubocop";
278+
} else if (await this.projectHasDependency(/^syntax_tree$/)) {
279+
this.#formatter = "syntax_tree";
280+
} else {
281+
this.#formatter = "none";
282+
}
283+
} else {
284+
this.#formatter = configuredFormatter;
285+
}
286+
}
287+
264288
get context(): vscode.ExtensionContext {
265289
return this.#context;
266290
}
@@ -341,7 +365,7 @@ export default class Client implements ClientInterface {
341365
gemfile.push('eval_gemfile(File.expand_path("../Gemfile", __dir__))');
342366

343367
// If the `ruby-lsp` exists in the bundle, add it to the custom Gemfile commented out
344-
if (await this.projectHasDependency("ruby-lsp")) {
368+
if (await this.projectHasDependency(/^ruby-lsp$/)) {
345369
// If it is already in the bundle, add the gem commented out to avoid conflicts
346370
gemfile.push(`# ${gemEntry}`);
347371
} else {
@@ -350,7 +374,7 @@ export default class Client implements ClientInterface {
350374
}
351375

352376
// If debug is not in the bundle, add it to allow debugging
353-
if (!(await this.projectHasDependency("debug"))) {
377+
if (!(await this.projectHasDependency(/^debug$/))) {
354378
gemfile.push(debugEntry);
355379
}
356380
} else {
@@ -424,7 +448,7 @@ export default class Client implements ClientInterface {
424448
return Object.keys(features).filter((key) => features[key]);
425449
}
426450

427-
private async projectHasDependency(gemName: string): Promise<boolean> {
451+
private async projectHasDependency(gemName: RegExp): Promise<boolean> {
428452
try {
429453
// We can't include `BUNDLE_GEMFILE` here, because we want to check if the project's bundle includes the
430454
// dependency and not our custom bundle
@@ -433,7 +457,7 @@ export default class Client implements ClientInterface {
433457
// exit with an error if gemName not a dependency or is a transitive dependency.
434458
// exit with success if gemName is a direct dependency.
435459
await asyncExec(
436-
`ruby -rbundler -e "exit 1 unless Bundler.locked_gems.dependencies.key?('${gemName}')"`,
460+
`ruby -rbundler -e "exit 1 unless Bundler.locked_gems.dependencies.keys.grep(${gemName}).any?"`,
437461
{
438462
cwd: this.workingFolder,
439463
env: withoutBundleGemfileEnv,

src/status.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export enum Command {
2020
ToggleYjit = "rubyLsp.toggleYjit",
2121
SelectVersionManager = "rubyLsp.selectRubyVersionManager",
2222
ToggleFeatures = "rubyLsp.toggleFeatures",
23+
FormatterHelp = "rubyLsp.formatterHelp",
2324
RunTest = "rubyLsp.runTest",
2425
DebugTest = "rubyLsp.debugTest",
2526
}
@@ -38,6 +39,7 @@ export interface ClientInterface {
3839
context: vscode.ExtensionContext;
3940
ruby: Ruby;
4041
state: ServerState;
42+
formatter: string;
4143
}
4244

4345
export abstract class StatusItem {
@@ -340,6 +342,35 @@ export class FeaturesStatus extends StatusItem {
340342
}
341343
}
342344

345+
export class FormatterStatus extends StatusItem {
346+
constructor(client: ClientInterface) {
347+
super("formatter", client);
348+
349+
this.item.name = "Formatter";
350+
this.item.command = {
351+
title: "Help",
352+
command: Command.FormatterHelp,
353+
};
354+
this.refresh();
355+
}
356+
357+
refresh(): void {
358+
this.item.text = `Using formatter: ${this.client.formatter}`;
359+
}
360+
361+
registerCommand(): void {
362+
this.context.subscriptions.push(
363+
vscode.commands.registerCommand(Command.FormatterHelp, () => {
364+
vscode.env.openExternal(
365+
vscode.Uri.parse(
366+
"https://github.com/Shopify/vscode-ruby-lsp#formatting"
367+
)
368+
);
369+
})
370+
);
371+
}
372+
}
373+
343374
export class StatusItems {
344375
private items: StatusItem[] = [];
345376

@@ -350,6 +381,7 @@ export class StatusItems {
350381
new ExperimentalFeaturesStatus(client),
351382
new YjitStatus(client),
352383
new FeaturesStatus(client),
384+
new FormatterStatus(client),
353385
];
354386
}
355387

src/test/suite/status.test.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ import {
1414
ServerState,
1515
ClientInterface,
1616
FeaturesStatus,
17+
FormatterStatus,
1718
} from "../../status";
1819

1920
suite("StatusItems", () => {
2021
let ruby: Ruby;
2122
let context: vscode.ExtensionContext;
2223
let status: StatusItem;
2324
let client: ClientInterface;
25+
let formatter: string;
2426

2527
beforeEach(() => {
2628
context = { subscriptions: [] } as unknown as vscode.ExtensionContext;
@@ -40,6 +42,7 @@ suite("StatusItems", () => {
4042
context,
4143
ruby,
4244
state: ServerState.Running,
45+
formatter: "none",
4346
};
4447
status = new RubyVersionStatus(client);
4548
});
@@ -67,7 +70,7 @@ suite("StatusItems", () => {
6770
suite("ServerStatus", () => {
6871
beforeEach(() => {
6972
ruby = {} as Ruby;
70-
client = { context, ruby, state: ServerState.Running };
73+
client = { context, ruby, state: ServerState.Running, formatter: "none" };
7174
status = new ServerStatus(client);
7275
});
7376

@@ -130,6 +133,7 @@ suite("StatusItems", () => {
130133
client = {
131134
context,
132135
ruby,
136+
formatter,
133137
state: ServerState.Running,
134138
};
135139
status = new ExperimentalFeaturesStatus(client);
@@ -150,7 +154,7 @@ suite("StatusItems", () => {
150154
suite("YjitStatus when Ruby supports it", () => {
151155
beforeEach(() => {
152156
ruby = { supportsYjit: true } as Ruby;
153-
client = { context, ruby, state: ServerState.Running };
157+
client = { context, ruby, state: ServerState.Running, formatter: "none" };
154158
status = new YjitStatus(client);
155159
});
156160

@@ -174,7 +178,7 @@ suite("StatusItems", () => {
174178
suite("YjitStatus when Ruby does not support it", () => {
175179
beforeEach(() => {
176180
ruby = { supportsYjit: false } as Ruby;
177-
client = { context, ruby, state: ServerState.Running };
181+
client = { context, ruby, state: ServerState.Running, formatter: "none" };
178182
status = new YjitStatus(client);
179183
});
180184

@@ -206,6 +210,7 @@ suite("StatusItems", () => {
206210
status = new FeaturesStatus({
207211
context,
208212
ruby,
213+
formatter,
209214
state: ServerState.Running,
210215
});
211216
});
@@ -253,4 +258,25 @@ suite("StatusItems", () => {
253258
});
254259
});
255260
});
261+
262+
suite("FormatterStatus", () => {
263+
beforeEach(() => {
264+
ruby = {} as Ruby;
265+
client = {
266+
context,
267+
ruby,
268+
state: ServerState.Running,
269+
formatter: "auto",
270+
};
271+
status = new FormatterStatus(client);
272+
});
273+
274+
test("Status is initialized with the right values", async () => {
275+
assert.strictEqual(status.item.text, "Using formatter: auto");
276+
assert.strictEqual(status.item.name, "Formatter");
277+
assert.strictEqual(status.item.command?.title, "Help");
278+
assert.strictEqual(status.item.command?.command, Command.FormatterHelp);
279+
assert.strictEqual(context.subscriptions.length, 1);
280+
});
281+
});
256282
});

0 commit comments

Comments
 (0)