-
-
Notifications
You must be signed in to change notification settings - Fork 66
/
Copy pathastProvider.ts
255 lines (229 loc) · 7.93 KB
/
astProvider.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
import fs from "fs";
import glob from "glob";
import util from "util";
// Convert fs.readFile into Promise version of same
const readFile = util.promisify(fs.readFile);
const globPromise = util.promisify(glob);
import Parser, { Point, SyntaxNode, Tree } from "tree-sitter";
import TreeSitterElm from "tree-sitter-elm";
import {
DidChangeTextDocumentParams,
DidCloseTextDocumentParams,
IConnection,
TextDocumentIdentifier,
VersionedTextDocumentIdentifier,
} from "vscode-languageserver";
import URI from "vscode-uri";
import { IForest } from "../forest";
import { Imports } from "../imports";
import { Position } from "../position";
import { DocumentEvents } from "../util/documentEvents";
import * as utils from "../util/elmUtils";
export class ASTProvider {
private connection: IConnection;
private forest: IForest;
private parser: Parser;
private elmWorkspace: URI;
private imports: Imports;
constructor(
connection: IConnection,
forest: IForest,
elmWorkspace: URI,
events: DocumentEvents,
imports: Imports,
) {
this.connection = connection;
this.forest = forest;
this.elmWorkspace = elmWorkspace;
this.imports = imports;
this.parser = new Parser();
try {
this.parser.setLanguage(TreeSitterElm);
} catch (error) {
this.connection.console.info(error.toString());
}
events.on("change", this.handleChangeTextDocument);
this.initializeWorkspace();
}
protected initializeWorkspace = async (): Promise<void> => {
try {
const path = this.elmWorkspace.fsPath + "elm.json";
this.connection.console.info("Reading elm.json from " + path);
// Find elm files and feed them to tree sitter
const elmJson = require(path);
const type = elmJson.type;
const elmFolders: Array<{ path: string; writeable: boolean }> = [];
let elmVersion = "";
if (type === "application") {
elmVersion = elmJson["elm-version"];
const sourceDirs = elmJson["source-directories"];
sourceDirs.forEach(async (folder: string) => {
elmFolders.push({
path: this.elmWorkspace.fsPath + folder,
writeable: true,
});
});
} else {
// Todo find a better way to do this
elmVersion = elmJson["elm-version"];
if (elmVersion.indexOf(" ") !== -1) {
elmVersion = elmVersion.substring(0, elmVersion.indexOf(" "));
}
elmFolders.push({
path: this.elmWorkspace.fsPath + "src",
writeable: true,
});
}
this.connection.console.info(elmFolders.length + " source-dirs found");
const elmHome = this.findElmHome();
// TODO find a way to detect this
const packagesRoot = `${elmHome}/${elmVersion}/package/`;
const dependencies: { [index: string]: string } =
type === "application"
? elmJson.dependencies.direct
: elmJson.dependencies;
for (const key in dependencies) {
if (dependencies.hasOwnProperty(key)) {
const maintainer = key.substring(0, key.indexOf("/"));
const packageName = key.substring(key.indexOf("/") + 1, key.length);
// We should probably parse the elm json of a dependency, at some point down the line
const pathToPackage =
type === "application"
? `${packagesRoot}${maintainer}/${packageName}/${
dependencies[key]
}/src`
: `${packagesRoot}${maintainer}/${packageName}/${dependencies[
key
].substring(0, elmVersion.indexOf(" "))}`;
elmFolders.push({ path: pathToPackage, writeable: false });
}
}
const elmFilePaths = await this.findElmFilesInFolders(elmFolders);
this.connection.console.info(
"Found " +
elmFilePaths.length.toString() +
" files to add to the project",
);
for (const filePath of elmFilePaths) {
this.connection.console.info("Adding " + filePath.path.toString());
const fileContent: string = await readFile(
filePath.path.toString(),
"utf8",
);
let tree: Tree | undefined;
tree = this.parser.parse(fileContent);
this.forest.setTree(
URI.file(filePath.path).toString(),
filePath.writeable,
true,
tree,
);
}
this.forest.treeIndex.forEach(item => {
this.connection.console.info("Adding imports " + item.uri.toString());
this.imports.updateImports(item.uri, item.tree, this.forest);
});
this.connection.console.info("Done parsing all files.");
} catch (error) {
this.connection.console.info(error.toString());
}
};
protected handleChangeTextDocument = async (
params: DidChangeTextDocumentParams,
): Promise<void> => {
this.connection.console.info(
`Changed text document, going to parse it. ${params.textDocument.uri}`,
);
const document: VersionedTextDocumentIdentifier = params.textDocument;
let tree: Tree | undefined = this.forest.getTree(document.uri);
if (tree === undefined) {
return;
}
for (const changeEvent of params.contentChanges) {
if (changeEvent.range && changeEvent.rangeLength) {
// range is range of the change. end is exclusive
// rangeLength is length of text removed
// text is new text
const { range, rangeLength, text } = changeEvent;
const startIndex: number = range.start.line * range.start.character;
const oldEndIndex: number = startIndex + rangeLength - 1;
if (tree) {
tree.edit({
// end index for new version of text
newEndIndex: range.end.line * range.end.character - 1,
// position in new doc change ended
newEndPosition: Position.FROM_VS_POSITION(range.end).toTSPosition(),
// end index for old version of text
oldEndIndex,
// position in old doc change ended.
oldEndPosition: this.computeEndPosition(
startIndex,
oldEndIndex,
tree,
),
// index in old doc the change started
startIndex,
// position in old doc change started
startPosition: Position.FROM_VS_POSITION(
range.start,
).toTSPosition(),
});
}
tree = this.parser.parse(text, tree);
} else {
tree = this.buildTree(changeEvent.text);
}
}
if (tree) {
this.forest.setTree(document.uri, true, true, tree);
this.imports.updateImports(document.uri, tree, this.forest);
}
};
private findElmHome() {
const elmHomeVar = process.env.ELM_HOME;
if (elmHomeVar) {
return elmHomeVar;
}
const homedir = require("os").homedir();
if (utils.isWindows) {
return homedir + "/AppData/Roaming/elm";
} else {
return homedir + "/.elm";
}
}
private async findElmFilesInFolders(
elmFolders: Array<{ path: string; writeable: boolean }>,
): Promise<Array<{ path: string; writeable: boolean }>> {
let elmFilePaths: Array<{ path: string; writeable: boolean }> = [];
for (const element of elmFolders) {
elmFilePaths = elmFilePaths.concat(
await this.findElmFilesInFolder(element),
);
}
return elmFilePaths;
}
private async findElmFilesInFolder(element: {
path: string;
writeable: boolean;
}): Promise<Array<{ path: string; writeable: boolean }>> {
return (await globPromise(element.path + "/**/*.elm", {})).map(
(a: string) => {
return { path: a, writeable: element.writeable };
},
);
}
private buildTree = (text: string): Tree | undefined => {
return this.parser.parse(text);
};
private computeEndPosition = (
startIndex: number,
endIndex: number,
tree: Tree,
): Point => {
const node: SyntaxNode = tree.rootNode.descendantForIndex(
startIndex,
endIndex,
);
return node.endPosition;
};
}