Skip to content

Commit 766127f

Browse files
committed
Implement D error link provider
Fix #214 Fix #442 Makes mixin error lines clickable
1 parent d5a96fc commit 766127f

File tree

3 files changed

+305
-0
lines changed

3 files changed

+305
-0
lines changed

src/commands.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { DubTasksProvider } from "./dub-tasks";
1111
import { showDpldocsSearch } from "./dpldocs";
1212
import { showQuickPickWithInput, simpleBytesToString } from "./util";
1313
import { listCompilers, makeCompilerDescription } from "./compilers";
14+
import { DTerminalLinkProvider } from "./terminal-link-provider";
1415

1516
const multiTokenWordPattern = /[^\`\~\!\@\#\%\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+(?:\.[^\`\~\!\@\#\%\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)*/;
1617

@@ -419,6 +420,7 @@ export function registerCommands(context: vscode.ExtensionContext) {
419420
vscode.commands.executeCommand("setContext", "d.isActive", true);
420421

421422
subscriptions.push(DubEditor.register(context));
423+
subscriptions.push(DTerminalLinkProvider.register());
422424

423425
subscriptions.push(vscode.commands.registerCommand("code-d.rdmdCurrent", async (file: vscode.Uri) => {
424426
var args: vscode.ShellQuotedString[] = [];

src/terminal-link-provider.ts

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import * as vscode from "vscode";
2+
import * as osPath from "path";
3+
import * as fs from "fs";
4+
import { openTextDocumentAtRange } from "./util";
5+
6+
export type TerminalFileLink = vscode.TerminalLink & { file: { path: vscode.Uri, line?: number, column?: number } };
7+
8+
export class DTerminalLinkProvider implements vscode.TerminalLinkProvider {
9+
provideTerminalLinks(context: { line: string, cwd?: string }, token?: vscode.CancellationToken): Thenable<TerminalFileLink[]> {
10+
let cwd = context.cwd || (vscode.workspace.workspaceFolders
11+
? vscode.workspace.workspaceFolders[0].uri.fsPath
12+
: process.cwd());
13+
// context.terminal.creationOptions.cwd is useless here, possibly
14+
// pointing to entirely different paths because vscode reuses terminals
15+
// (or sessions) across different workspaces, keeping old defaults.
16+
return Promise.all(findDErrorLines(cwd, context.line))
17+
.then(v => <TerminalFileLink[]>v.filter(l => l !== null));
18+
}
19+
20+
handleTerminalLink(link: TerminalFileLink): vscode.ProviderResult<void> {
21+
var range: null | number | vscode.Position = null;
22+
if (link.file.line && link.file.column)
23+
range = new vscode.Position(link.file.line - 1, link.file.column - 1);
24+
else if (link.file.line)
25+
range = link.file.line - 1;
26+
27+
openTextDocumentAtRange(link.file.path, range);
28+
}
29+
30+
static register(): { dispose(): any; } {
31+
const provider = new DTerminalLinkProvider();
32+
return vscode.window.registerTerminalLinkProvider(provider);
33+
}
34+
}
35+
36+
const dubFileSearch = process.platform == "win32" ? "dub\\packages\\" : "dub/packages/";
37+
function findDErrorLines(cwd: string, line: string): Promise<TerminalFileLink | null>[] {
38+
const ret: Promise<TerminalFileLink | null>[] = [];
39+
let i = 0;
40+
while (true)
41+
{
42+
i = line.indexOf("(", i);
43+
if (i == -1)
44+
break;
45+
46+
let firstLineDigit = line[i + 1];
47+
if (isDigit(firstLineDigit) && (
48+
line.endsWith(".d", i)
49+
|| line.endsWith(".di", i)
50+
|| line.endsWith(".dt", i) // diet templates
51+
|| endsWithMixin(line, i)
52+
))
53+
ret.push(extractFileLinkAt(cwd, line, i));
54+
55+
i++;
56+
}
57+
return ret;
58+
}
59+
60+
function endsWithMixin(line: string, endIndex: number): boolean {
61+
// format = "file.d-mixin-5(5, 8)"
62+
if (endIndex == 0 || !isDigit(line[endIndex - 1]))
63+
return false;
64+
65+
endIndex--;
66+
while (endIndex > 0 && isDigit(line[endIndex - 1]))
67+
endIndex--;
68+
69+
return line.endsWith("-mixin-", endIndex);
70+
}
71+
72+
function isDigit(c: string): boolean {
73+
return c >= '0' && c <= '9';
74+
}
75+
76+
async function extractFileLinkAt(
77+
cwd: string,
78+
line: string,
79+
i: number,
80+
): Promise<TerminalFileLink | null> {
81+
function isValidFilePathPart(c: string) {
82+
return c != ' '
83+
&& c != '(' && c != ')'
84+
&& c != '[' && c != ']'
85+
&& c != ':' && c != '@'
86+
&& c != '`' && c != '"' && c != '\''
87+
&& c != ',' && c != '!' && c != '?';
88+
}
89+
90+
let endOffset = 0;
91+
92+
let gotDriveLetter = false;
93+
let prefixDone = false;
94+
function isValidPrefix(c: string) {
95+
if (prefixDone)
96+
return false;
97+
98+
if (process.platform == "win32" && c == ':' && !gotDriveLetter) {
99+
gotDriveLetter = true;
100+
return true;
101+
}
102+
103+
if (gotDriveLetter) {
104+
if (c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z') {
105+
prefixDone = true;
106+
return true;
107+
} else {
108+
i++;
109+
return false;
110+
}
111+
}
112+
113+
return isValidFilePathPart(c) || c == ':';
114+
}
115+
116+
let lineNo: number | undefined = undefined;
117+
let column: number | undefined = undefined;
118+
while (i > 0 && isValidPrefix(line[i - 1]))
119+
i--;
120+
let file = line.substring(i);
121+
122+
let end = 0;
123+
while (isValidFilePathPart(file[end]))
124+
end++;
125+
126+
if (end == 0 || file[end - 1] == '.')
127+
return null;
128+
129+
const lineNoMatcher = /^[(:](\d+)(?:[,:](\d+))?/;
130+
const lineNoMatch = file.substring(end).match(lineNoMatcher);
131+
132+
if (lineNoMatch)
133+
{
134+
lineNo = parseInt(lineNoMatch[1]);
135+
if (lineNoMatch[2])
136+
column = parseInt(lineNoMatch[2]);
137+
endOffset += lineNoMatch[0].length;
138+
if (lineNoMatch[0][0] == '(')
139+
endOffset++;
140+
}
141+
142+
if (endsWithMixin(file, end))
143+
{
144+
let newEnd = file.lastIndexOf("-mixin-", end);
145+
if (newEnd == -1)
146+
throw new Error("this should not happen");
147+
lineNo = parseInt(file.substring(newEnd + 7, end));
148+
column = undefined;
149+
endOffset += (end - newEnd);
150+
end = newEnd;
151+
}
152+
153+
let filePath = await resolveFilePath(cwd, file.substring(0, end));
154+
if (!filePath)
155+
return null;
156+
157+
return {
158+
startIndex: i,
159+
length: end + endOffset,
160+
file: {
161+
path: filePath,
162+
line: lineNo,
163+
column: column
164+
}
165+
};
166+
}
167+
168+
export const dubPackagesHome = determineDubPackageHome();
169+
170+
let resolveAllFilePathsForTest: boolean = false;
171+
export function enableResolveAllFilePathsForTest() {
172+
return resolveAllFilePathsForTest = true;
173+
}
174+
175+
function resolveFilePath(cwd: string, path: string): Promise<vscode.Uri | null> {
176+
return new Promise(function(resolve) {
177+
if (!osPath.isAbsolute(path))
178+
path = osPath.join(cwd, path);
179+
fs.stat(path, function(err, stats) {
180+
if (!err && stats.isFile())
181+
return resolve(vscode.Uri.file(path));
182+
183+
var dubPathStart = path.indexOf(dubFileSearch);
184+
if (dubPathStart != -1) {
185+
path = osPath.join(dubPackagesHome, path.substring(dubPathStart + dubFileSearch.length));
186+
fs.stat(path, function(err, stats) {
187+
if ((!err && stats.isFile()) || resolveAllFilePathsForTest) {
188+
resolve(vscode.Uri.file(path));
189+
} else {
190+
resolve(null);
191+
}
192+
});
193+
} else {
194+
if (resolveAllFilePathsForTest)
195+
return resolve(vscode.Uri.file(path));
196+
else
197+
resolve(null);
198+
}
199+
});
200+
});
201+
}
202+
203+
function determineDubPackageHome(): string {
204+
let dubHome = process.env["DUB_HOME"];
205+
if (!dubHome) {
206+
let dpath = process.env["DPATH"];
207+
if (dpath) {
208+
dubHome = osPath.join(dpath, "dub");
209+
}
210+
}
211+
212+
if (dubHome) {
213+
return osPath.join(dubHome, "packages");
214+
}
215+
216+
if (process.platform == "win32") {
217+
return osPath.join(process.env["LOCALAPPDATA"] || process.env["APPDATA"] || process.cwd(), "dub", "packages");
218+
} else {
219+
return osPath.join(process.env["HOME"] || process.cwd(), ".dub", "packages");
220+
}
221+
}

src/test/suite/terminal_links.test.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import * as assert from 'assert';
2+
import {
3+
DTerminalLinkProvider,
4+
enableResolveAllFilePathsForTest,
5+
TerminalFileLink,
6+
dubPackagesHome
7+
} from '../../terminal-link-provider';
8+
import * as osPath from 'path';
9+
import * as vscode from 'vscode';
10+
11+
suite("terminal links", () => {
12+
enableResolveAllFilePathsForTest();
13+
let provider = new DTerminalLinkProvider();
14+
15+
test("DUB path rewriting", async () => {
16+
assert.deepStrictEqual(await provider.provideTerminalLinks({
17+
line: "../../elsewhere/.dub/packages/msgpack-d-1.0.1/msgpack-d/src/msgpack/common.d(532,9): Deprecation: usage of the `body` keyword is deprecated. Use `do` instead.",
18+
cwd: "/tmp/myproject"
19+
}), <TerminalFileLink[]>[
20+
{
21+
startIndex: 0,
22+
length: 83,
23+
file: {
24+
path: vscode.Uri.file(osPath.join(dubPackagesHome, "msgpack-d-1.0.1/msgpack-d/src/msgpack/common.d")),
25+
line: 532,
26+
column: 9
27+
}
28+
}
29+
]);
30+
});
31+
32+
test("DMD error reporting", async () => {
33+
assert.deepStrictEqual(await provider.provideTerminalLinks({
34+
line: "source/app.d(5,15): Error: unable to read module `bm`",
35+
cwd: "/tmp/myproject"
36+
}), <TerminalFileLink[]>[
37+
{
38+
startIndex: 0,
39+
length: 18,
40+
file: {
41+
path: vscode.Uri.file("/tmp/myproject/source/app.d"),
42+
line: 5,
43+
column: 15
44+
}
45+
}
46+
]);
47+
});
48+
49+
test("D exceptions", async () => {
50+
assert.deepStrictEqual(await provider.provideTerminalLinks({
51+
line: "core.exception.AssertError@source/app.d(6): Assertion failure",
52+
cwd: "/tmp/myproject"
53+
}), <TerminalFileLink[]>[
54+
{
55+
startIndex: 27,
56+
length: 15,
57+
file: {
58+
path: vscode.Uri.file("/tmp/myproject/source/app.d"),
59+
line: 6,
60+
column: undefined
61+
}
62+
}
63+
]);
64+
});
65+
66+
test("mixin errors", async () => {
67+
assert.deepStrictEqual(await provider.provideTerminalLinks({
68+
line: "source/app.d-mixin-5(7,8): Error: unable to read module `foobar`",
69+
cwd: "/tmp/myproject"
70+
}), <TerminalFileLink[]>[
71+
{
72+
startIndex: 0,
73+
length: 25,
74+
file: {
75+
path: vscode.Uri.file("/tmp/myproject/source/app.d"),
76+
line: 5,
77+
column: undefined
78+
}
79+
}
80+
]);
81+
});
82+
});

0 commit comments

Comments
 (0)