Skip to content

Commit d146865

Browse files
authored
Feature: Jump to label (#82)
* Show buildifier parse error as diagnostic * Buildifier report error diagnostic on parse error * Disable bazel doc group hover
1 parent 191dc45 commit d146865

11 files changed

Lines changed: 211 additions & 32 deletions

File tree

package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,12 @@
480480
"title": "Bazel: Copy label to clipboard",
481481
"icon": "$(clippy)"
482482
},
483+
{
484+
"category": "Bzl",
485+
"command": "bsv.bzl.goToLabel",
486+
"title": "Bazel: Goto Label",
487+
"icon": "$(go-to-file)"
488+
},
483489
{
484490
"category": "Bzl",
485491
"command": "bsv.buildozer.wizard",
@@ -578,6 +584,10 @@
578584
"command": "bsv.buildozer.wizard",
579585
"key": "ctrl+shift+cmd+p"
580586
},
587+
{
588+
"command": "bsv.bzl.goToLabel",
589+
"key": "cmd+;"
590+
},
581591
{
582592
"command": "workbench.view.extension.bazel-explorer",
583593
"key": "shift+cmd+t",

src/bazeldoc/feature.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,17 @@ import { Reconfigurable } from '../reconfigurable';
66

77
export const BazelDocFeatureName = 'bsv.bazeldoc';
88

9+
// this was an early feature; now that we have better LSP completion support,
10+
// these hovers are just annoying.
11+
const useBazelDocGroupHover = false;
12+
913
export class BazelDocFeature extends Reconfigurable<BazelDocConfiguration> {
1014
constructor() {
1115
super(BazelDocFeatureName);
1216

13-
this.add(new BazelDocGroupHover(this.onDidConfigurationChange.event));
17+
if (useBazelDocGroupHover) {
18+
this.add(new BazelDocGroupHover(this.onDidConfigurationChange.event));
19+
}
1420
}
1521

1622
protected async configure(config: vscode.WorkspaceConfiguration): Promise<BazelDocConfiguration> {

src/bezel/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export enum CommandName {
4747
CodesearchIndex = 'bsv.bzl.codesearch.index',
4848
CodesearchSearch = 'bsv.bzl.codesearch.search',
4949
CopyLabel = 'bsv.bzl.copyLabel',
50+
GoToLabel = 'bsv.bzl.goToLabel',
5051
CopyToClipboard = 'bsv.bzl.copyToClipboard',
5152
DebugBuild = 'bsv.bzl.debugBuild',
5253
AskForDebugTargetLabel = 'bsv.bzl.askForDebugTargetLabel',

src/bezel/feature.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ import { StarlarkDebugger } from './debugger';
3232
import { RunnableComponent, Status } from './status';
3333
import { Settings } from './settings';
3434
import { BuildozerSettings } from '../buildozer/settings';
35-
import { Buildozer } from '../buildozer/buildozer';
36-
import { ConfigurationContext, ConfigurationPropertyMap } from '../common';
35+
import { ConfigurationContext } from '../common';
3736
import findUp = require('find-up');
3837
import path = require('path');
38+
import { Buildozer } from '../buildozer/buildozer';
3939

4040
export const BzlFeatureName = 'bsv.bzl';
4141

src/bezel/lsp.ts

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import {
77
ServerOptions,
88
State,
99
StateChangeEvent,
10+
TextDocumentIdentifier,
1011
TextDocumentPositionParams,
1112
} from 'vscode-languageclient/node';
13+
import { BuiltInCommands } from '../constants';
1214
import {
1315
LanguageServerConfiguration,
1416
} from './configuration';
@@ -32,7 +34,8 @@ export class BzlLanguageClient
3234
super('LSP', settings);
3335

3436
this.disposables.push(
35-
vscode.commands.registerCommand(CommandName.CopyLabel, this.handleCommandCopyLabel, this)
37+
vscode.commands.registerCommand(CommandName.CopyLabel, this.handleCommandCopyLabel, this),
38+
vscode.commands.registerCommand(CommandName.GoToLabel, this.handleCommandGoToLabel, this),
3639
);
3740
}
3841

@@ -80,6 +83,47 @@ export class BzlLanguageClient
8083
}
8184
}
8285

86+
private async handleCommandGoToLabel(): Promise<void | undefined> {
87+
const label = await vscode.window.showInputBox({
88+
prompt: 'Enter bazel label to locate',
89+
placeHolder: '//path/to:target',
90+
});
91+
if (!label) {
92+
return;
93+
}
94+
return this.goToLabel(label);
95+
}
96+
97+
private async goToLabel(label: string): Promise<void | undefined> {
98+
const editor = vscode.window.activeTextEditor;
99+
if (!editor) {
100+
return;
101+
}
102+
if (!editor.document.uri) {
103+
return;
104+
}
105+
try {
106+
const location = await this.getLabelLocation(editor.document.uri, label);
107+
if (!location || !location.uri || !location.range) {
108+
return;
109+
}
110+
111+
return vscode.commands.executeCommand(
112+
BuiltInCommands.Open,
113+
vscode.Uri.parse(location.uri).with({
114+
// location response is zero-based; convert to 1-base
115+
fragment: `${location.range.start.line + 1},${location.range.start.character + 1}`,
116+
})
117+
);
118+
} catch (e) {
119+
if (e instanceof Error) {
120+
console.debug(e.message);
121+
} else {
122+
console.debug(JSON.stringify(e));
123+
}
124+
}
125+
}
126+
83127
private async handleCommandCopyLabel(doc: vscode.TextDocument): Promise<void> {
84128
const editor = vscode.window.activeTextEditor;
85129
if (!editor) {
@@ -107,6 +151,28 @@ export class BzlLanguageClient
107151
}
108152
}
109153

154+
// getLabelLocation returns the location of the given label. The URI is used
155+
// to resolve non-canonical forms.
156+
public async getLabelLocation(
157+
uri: vscode.Uri,
158+
label: string,
159+
cancellation = new vscode.CancellationTokenSource()
160+
): Promise<Location | undefined> {
161+
if (!this.languageClient) {
162+
return undefined;
163+
}
164+
const params: BuildFileLabelLocationParams = {
165+
textDocument: { uri: uri.toString() },
166+
label: label,
167+
};
168+
const location: Location | undefined = await this.languageClient.sendRequest(
169+
'buildFile/labelLocation',
170+
params,
171+
cancellation.token
172+
);
173+
return location;
174+
}
175+
110176
public async getLabelAtDocumentPosition(
111177
uri: vscode.Uri,
112178
position: vscode.Position,
@@ -232,6 +298,11 @@ export interface Invocation {
232298
createdAt: number;
233299
}
234300

301+
export interface BuildFileLabelLocationParams {
302+
textDocument: TextDocumentIdentifier;
303+
label: string;
304+
}
305+
235306
function createLanguageClient(cfg: LanguageServerConfiguration): LanguageClient {
236307
let serverOptions: ServerOptions = {
237308
command: cfg.executable,

src/buildifier/buildifier.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@ export class Buildifier extends RunnableComponent<BuildifierConfiguration> {
1818
await this.settings.get();
1919
}
2020

21-
async stopInternal(): Promise<void> {}
21+
async stopInternal(): Promise<void> { }
2222
}

src/buildifier/diagnostics.ts

Lines changed: 82 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import * as vscode from 'vscode';
1616
import { buildifierLint, getBuildifierFileType } from './execute';
17+
import { IBuildifierWarning } from './result';
1718
import { BuildifierSettings } from './settings';
1819

1920
/**
@@ -62,6 +63,18 @@ export class BuildifierDiagnosticsManager {
6263
}
6364
}
6465

66+
/**
67+
* Sets a single error diagnostic. This is used when a parsing error occurs
68+
* and we want to report the location.
69+
*
70+
* @param document The text document whose diagnostics should be updated.
71+
*/
72+
public async error(document: vscode.TextDocument, range: vscode.Range, message: string) {
73+
const diagnostic = new vscode.Diagnostic(range, message, vscode.DiagnosticSeverity.Error);
74+
diagnostic.source = 'buildifier';
75+
this.diagnosticsCollection.set(document.uri, [diagnostic]);
76+
}
77+
6578
/**
6679
* Updates the diagnostics collection with lint warnings for the given text
6780
* document.
@@ -81,32 +94,80 @@ export class BuildifierDiagnosticsManager {
8194
return;
8295
}
8396

84-
const warnings = await buildifierLint(
97+
const result = await buildifierLint(
8598
cfg,
8699
document.getText(),
87100
getBuildifierFileType(document.uri.fsPath),
88101
'warn'
89102
);
90103

91-
this.diagnosticsCollection.set(
92-
document.uri,
93-
warnings.map(warning => {
94-
// Buildifier returns 1-based line numbers, but VS Code is 0-based.
95-
const range = new vscode.Range(
96-
warning.start.line - 1,
97-
warning.start.column - 1,
98-
warning.end.line - 1,
99-
warning.end.column - 1
100-
);
101-
const diagnostic = new vscode.Diagnostic(
102-
range,
103-
warning.message,
104-
vscode.DiagnosticSeverity.Warning
105-
);
106-
diagnostic.source = 'buildifier';
107-
diagnostic.code = warning.category;
108-
return diagnostic;
109-
})
110-
);
104+
if (!result.stderr) {
105+
this.diagnosticsCollection.set(
106+
document.uri,
107+
result.file.warnings.map(warning => {
108+
// Buildifier returns 1-based line numbers, but VS Code is 0-based.
109+
const range = new vscode.Range(
110+
warning.start.line - 1,
111+
warning.start.column - 1,
112+
warning.end.line - 1,
113+
warning.end.column - 1
114+
);
115+
const diagnostic = new vscode.Diagnostic(
116+
range,
117+
warning.message,
118+
vscode.DiagnosticSeverity.Warning
119+
);
120+
diagnostic.source = 'buildifier';
121+
diagnostic.code = warning.category;
122+
return diagnostic;
123+
})
124+
);
125+
} else {
126+
const warnings: IBuildifierWarning[] = [];
127+
const syntaxError = parseStderr(result.stderr);
128+
if (syntaxError) {
129+
warnings.push(syntaxError);
130+
}
131+
this.diagnosticsCollection.set(
132+
document.uri,
133+
warnings.map(warning => {
134+
// Buildifier returns 1-based line numbers, but VS Code is 0-based.
135+
const range = new vscode.Range(
136+
warning.start.line - 1,
137+
warning.start.column - 1,
138+
warning.end.line - 1,
139+
warning.end.column - 1
140+
);
141+
const diagnostic = new vscode.Diagnostic(
142+
range,
143+
warning.message,
144+
vscode.DiagnosticSeverity.Error
145+
);
146+
diagnostic.source = 'buildifier';
147+
diagnostic.code = warning.category;
148+
return diagnostic;
149+
})
150+
);
151+
152+
}
111153
}
112154
}
155+
156+
function parseStderr(input: string): IBuildifierWarning | undefined {
157+
if (!input.startsWith('<stdin>')) {
158+
return undefined;
159+
}
160+
const parts = input.slice('<stdin>:'.length).split(':');
161+
const line = parseInt(parts[0]);
162+
const column = parseInt(parts[1]);
163+
const rest = parts.slice(2).join(':');
164+
const pos = { line, column };
165+
166+
return {
167+
start: pos,
168+
end: pos,
169+
category: 'invalid-input',
170+
actionable: false,
171+
message: rest,
172+
}
173+
}

src/buildifier/execute.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import * as child_process from 'child_process';
1616
import * as path from 'path';
17-
import { IBuildifierResult, IBuildifierWarning } from './result';
17+
import { IBuildifierResult, IBuildifierStdinResult } from './result';
1818
import { BuildifierConfiguration } from './configuration';
1919

2020
/** Whether to warn about lint findings or fix them. */
@@ -81,14 +81,14 @@ export async function buildifierLint(
8181
fileContent: string,
8282
type: BuildifierFileType,
8383
lintMode: 'warn'
84-
): Promise<IBuildifierWarning[]>;
84+
): Promise<IBuildifierStdinResult>;
8585

8686
export async function buildifierLint(
8787
cfg: BuildifierConfiguration,
8888
fileContent: string,
8989
type: BuildifierFileType,
9090
lintMode: BuildifierLintMode
91-
): Promise<string | IBuildifierWarning[]> {
91+
): Promise<string | IBuildifierStdinResult> {
9292
const args = ['--format=json', '--mode=check', `--type=${type}`, `--lint=${lintMode}`];
9393
const outputs = await executeBuildifier(cfg, fileContent, args, true);
9494
switch (lintMode) {
@@ -98,10 +98,23 @@ export async function buildifierLint(
9898
const result = JSON.parse(outputs.stdout) as IBuildifierResult;
9999
for (const file of result.files) {
100100
if (file.filename === '<stdin>') {
101-
return file.warnings;
101+
return {
102+
success: result.success,
103+
file: file,
104+
stderr: outputs.stderr
105+
}
102106
}
103107
}
104-
return [];
108+
// should not occur
109+
return {
110+
success: false,
111+
file: {
112+
filename: '<stdin>',
113+
formatted: false,
114+
valid: false,
115+
warnings: [],
116+
}
117+
};
105118
}
106119
}
107120

src/buildifier/formatter.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ export class BuildifierFormatter implements vscode.DocumentFormattingEditProvide
7070
),
7171
];
7272
} catch (err) {
73-
vscode.window.showErrorMessage(`buildifier format error: ${err}`);
73+
// linter should report the syntactic error, ignore this for the user, but
74+
// report debugging info
75+
console.warn('buildifier error:', err);
7476
}
7577

7678
return [];

src/buildifier/result.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,20 @@ export interface IBuildifierResult {
2323
files: IBuildifierFile[];
2424
}
2525

26+
/** The result of a single file result from stdin (typical case) */
27+
export interface IBuildifierStdinResult {
28+
/**
29+
* Indicates whether or not the check succeeded without finding any problems.
30+
*/
31+
success: boolean;
32+
33+
/** Information about each file that was checked. */
34+
file: IBuildifierFile;
35+
36+
/** Stderr, if the input was not valid */
37+
stderr?: string;
38+
}
39+
2640
/** Information about a file that was checked by buildifier. */
2741
export interface IBuildifierFile {
2842
/** The path of the file that was checked. */

0 commit comments

Comments
 (0)