linear-ls 1.0.5 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server.js +95 -40
- package/package.json +7 -30
- package/dist/linear.js +0 -115
- package/dist/types.generated.js +0 -2
package/dist/server.js
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const sdk_1 = require("@linear/sdk");
|
|
3
4
|
const node_1 = require("vscode-languageserver/node");
|
|
4
5
|
const vscode_languageserver_textdocument_1 = require("vscode-languageserver-textdocument");
|
|
5
|
-
const linear_1 = require("./linear");
|
|
6
6
|
const connection = (0, node_1.createConnection)(node_1.ProposedFeatures.all);
|
|
7
7
|
const documents = new node_1.TextDocuments(vscode_languageserver_textdocument_1.TextDocument);
|
|
8
|
-
const
|
|
8
|
+
const teams = new Map();
|
|
9
9
|
const issues = new Map();
|
|
10
10
|
const issuePositions = new Map();
|
|
11
|
+
let client;
|
|
11
12
|
connection.onInitialize(async () => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const apiKey = process.env.LINEAR_API_KEY;
|
|
14
|
+
if (!apiKey) {
|
|
15
|
+
connection.window.showWarningMessage("Linear API Key not found. Please set the LINEAR_API_KEY environment variable.");
|
|
16
|
+
return { capabilities: {} };
|
|
17
|
+
}
|
|
18
|
+
client = new sdk_1.LinearClient({ apiKey });
|
|
19
|
+
await client.teams().then((ts) => {
|
|
20
|
+
ts.nodes.forEach((t) => {
|
|
21
|
+
teams.set(t.key, t.id);
|
|
15
22
|
});
|
|
16
23
|
});
|
|
17
24
|
const result = {
|
|
@@ -20,6 +27,13 @@ connection.onInitialize(async () => {
|
|
|
20
27
|
codeActionProvider: {},
|
|
21
28
|
hoverProvider: {},
|
|
22
29
|
completionProvider: { resolveProvider: true },
|
|
30
|
+
semanticTokensProvider: {
|
|
31
|
+
legend: {
|
|
32
|
+
tokenTypes: [node_1.SemanticTokenTypes.variable],
|
|
33
|
+
tokenModifiers: [],
|
|
34
|
+
},
|
|
35
|
+
full: true,
|
|
36
|
+
},
|
|
23
37
|
},
|
|
24
38
|
};
|
|
25
39
|
return result;
|
|
@@ -38,31 +52,35 @@ connection.onCodeAction((params) => {
|
|
|
38
52
|
return [{ title: "Create Ticket" }];
|
|
39
53
|
}
|
|
40
54
|
});
|
|
55
|
+
let documentChangeTimeout;
|
|
41
56
|
documents.onDidChangeContent((change) => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
clearTimeout(documentChangeTimeout);
|
|
58
|
+
documentChangeTimeout = setTimeout(() => {
|
|
59
|
+
const text = change.document.getText();
|
|
60
|
+
const documentPositions = [];
|
|
61
|
+
Array.from(teams.keys())
|
|
62
|
+
.flatMap((prefix) => {
|
|
63
|
+
return Array.from(text.matchAll(new RegExp(`${prefix}-[0-9]*`, "g")));
|
|
64
|
+
})
|
|
65
|
+
.forEach((m) => {
|
|
66
|
+
const issueKey = m[0];
|
|
67
|
+
const positionStart = change.document.positionAt(m.index ?? 0);
|
|
68
|
+
const positionEnd = change.document.positionAt(issueKey.length + (m.index ?? 0));
|
|
69
|
+
// Write down that we've seen the issue, but don't
|
|
70
|
+
// fetch the definition just yet.
|
|
71
|
+
if (!issues.has(issueKey)) {
|
|
72
|
+
issues.set(issueKey, undefined);
|
|
73
|
+
}
|
|
74
|
+
documentPositions.push({
|
|
75
|
+
issueKey,
|
|
76
|
+
positionStart,
|
|
77
|
+
positionEnd,
|
|
78
|
+
offsetStart: change.document.offsetAt(positionStart),
|
|
79
|
+
offsetEnd: change.document.offsetAt(positionEnd),
|
|
80
|
+
});
|
|
63
81
|
});
|
|
64
|
-
|
|
65
|
-
|
|
82
|
+
issuePositions.set(change.document.uri, documentPositions);
|
|
83
|
+
}, 200);
|
|
66
84
|
});
|
|
67
85
|
connection.onHover(async (params) => {
|
|
68
86
|
const documentPositions = issuePositions.get(params.textDocument.uri);
|
|
@@ -78,16 +96,23 @@ connection.onHover(async (params) => {
|
|
|
78
96
|
if (!targetIssue) {
|
|
79
97
|
return;
|
|
80
98
|
}
|
|
81
|
-
const
|
|
99
|
+
const issueFromCache = issues.get(targetIssue.issueKey);
|
|
100
|
+
if (issueFromCache) {
|
|
101
|
+
return {
|
|
102
|
+
contents: issueFromCache.description ?? "Not available",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const issue = await client.issue(targetIssue.issueKey);
|
|
82
106
|
if (!issue) {
|
|
83
107
|
return;
|
|
84
108
|
}
|
|
109
|
+
issues.set(targetIssue.issueKey, issue);
|
|
85
110
|
return {
|
|
86
111
|
contents: issue.description ?? "Not available",
|
|
87
112
|
};
|
|
88
113
|
});
|
|
89
114
|
connection.onCompletion(async (params) => {
|
|
90
|
-
if (
|
|
115
|
+
if (teams.size === 0) {
|
|
91
116
|
return [];
|
|
92
117
|
}
|
|
93
118
|
const document = documents.get(params.textDocument.uri);
|
|
@@ -98,7 +123,7 @@ connection.onCompletion(async (params) => {
|
|
|
98
123
|
start: { line: params.position.line, character: 0 },
|
|
99
124
|
end: { line: params.position.line, character: params.position.character },
|
|
100
125
|
});
|
|
101
|
-
const lastMatch = Array.from(
|
|
126
|
+
const lastMatch = Array.from(teams.keys())
|
|
102
127
|
.flatMap((prefix) => Array.from(lineToPosition.matchAll(new RegExp(`(${prefix})-(.*)`, "g"))))
|
|
103
128
|
.at(-1);
|
|
104
129
|
if (!lastMatch) {
|
|
@@ -108,24 +133,54 @@ connection.onCompletion(async (params) => {
|
|
|
108
133
|
if (!teamKey || !searchString) {
|
|
109
134
|
return [];
|
|
110
135
|
}
|
|
111
|
-
|
|
112
|
-
|
|
136
|
+
let issues;
|
|
137
|
+
try {
|
|
138
|
+
issues = await client.searchIssues(searchString, {
|
|
139
|
+
teamId: teams.get(teamKey),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
connection.console.error(`LLS: Error searching issues: ${e}`);
|
|
113
144
|
return [];
|
|
114
145
|
}
|
|
115
|
-
return issues.map((
|
|
116
|
-
let label = `${
|
|
146
|
+
return issues.nodes.map((i) => {
|
|
147
|
+
let label = `${i.identifier}: ${i.title}`;
|
|
117
148
|
if (label.length > 60) {
|
|
118
|
-
label = label.substring(0, 60)
|
|
149
|
+
label = `${label.substring(0, 60)}...`;
|
|
119
150
|
}
|
|
120
151
|
return {
|
|
121
|
-
data:
|
|
122
|
-
detail:
|
|
123
|
-
insertText: `[${
|
|
124
|
-
filterText: `${issue.team.key}-${issue.title}`,
|
|
152
|
+
data: i,
|
|
153
|
+
detail: i.description ?? "Not available",
|
|
154
|
+
insertText: `[${i.identifier}](${i.url})`,
|
|
125
155
|
label,
|
|
126
156
|
kind: node_1.CompletionItemKind.Reference,
|
|
127
157
|
};
|
|
128
158
|
});
|
|
129
159
|
});
|
|
160
|
+
connection.languages.semanticTokens.on((params) => {
|
|
161
|
+
const positions = issuePositions.get(params.textDocument.uri);
|
|
162
|
+
if (!positions) {
|
|
163
|
+
return { data: [] };
|
|
164
|
+
}
|
|
165
|
+
const builder = new node_1.SemanticTokensBuilder();
|
|
166
|
+
positions.sort((a, b) => {
|
|
167
|
+
if (a.positionStart.line !== b.positionStart.line) {
|
|
168
|
+
return a.positionStart.line - b.positionStart.line;
|
|
169
|
+
}
|
|
170
|
+
return a.positionStart.character - b.positionStart.character;
|
|
171
|
+
});
|
|
172
|
+
for (const p of positions) {
|
|
173
|
+
// Semantic tokens are assumed to be single-line. If a token ever spans multiple
|
|
174
|
+
// lines, its length must be calculated differently. For now, we ignore such ranges.
|
|
175
|
+
if (p.positionStart.line !== p.positionEnd.line) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const length = p.positionEnd.character - p.positionStart.character;
|
|
179
|
+
builder.push(p.positionStart.line, p.positionStart.character, length, 0, // token type index (0 = variable, as defined in legend)
|
|
180
|
+
0 // token modifiers (none)
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return builder.build();
|
|
184
|
+
});
|
|
130
185
|
documents.listen(connection);
|
|
131
186
|
connection.listen();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linear-ls",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Linear Language Server",
|
|
5
5
|
"bin": "index.js",
|
|
6
6
|
"main": "index.js",
|
|
@@ -18,36 +18,13 @@
|
|
|
18
18
|
"index.js"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"vscode-languageserver": "^8.0.2",
|
|
25
|
-
"vscode-languageserver-textdocument": "^1.0.8"
|
|
21
|
+
"@linear/sdk": "^68.1.0",
|
|
22
|
+
"vscode-languageserver": "^9.0.1",
|
|
23
|
+
"vscode-languageserver-textdocument": "^1.0.12"
|
|
26
24
|
},
|
|
27
25
|
"devDependencies": {
|
|
28
|
-
"@
|
|
29
|
-
"@
|
|
30
|
-
"
|
|
31
|
-
"@types/node-fetch": "^2.6.2",
|
|
32
|
-
"@typescript-eslint/eslint-plugin": "^5.50.0",
|
|
33
|
-
"@typescript-eslint/parser": "^5.50.0",
|
|
34
|
-
"eslint": "^8.33.0",
|
|
35
|
-
"prettier": "^2.8.3",
|
|
36
|
-
"typescript": "^4.9.5"
|
|
37
|
-
},
|
|
38
|
-
"eslintConfig": {
|
|
39
|
-
"parser": "@typescript-eslint/parser",
|
|
40
|
-
"plugins": [
|
|
41
|
-
"@typescript-eslint"
|
|
42
|
-
],
|
|
43
|
-
"extends": [
|
|
44
|
-
"eslint:recommended",
|
|
45
|
-
"plugin:@typescript-eslint/recommended"
|
|
46
|
-
],
|
|
47
|
-
"rules": {
|
|
48
|
-
"@typescript-eslint/no-unused-vars": "error",
|
|
49
|
-
"@typescript-eslint/no-explicit-any": "error",
|
|
50
|
-
"@typescript-eslint/no-non-null-assertion": "error"
|
|
51
|
-
}
|
|
26
|
+
"@biomejs/biome": "^2.3.10",
|
|
27
|
+
"@types/node": "^23",
|
|
28
|
+
"typescript": "^5.9.3"
|
|
52
29
|
}
|
|
53
30
|
}
|
package/dist/linear.js
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.findIssuesByTitle = exports.getIssueByKey = exports.getTeamKeys = void 0;
|
|
7
|
-
const core_1 = require("@urql/core");
|
|
8
|
-
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
9
|
-
const client = (0, core_1.createClient)({
|
|
10
|
-
url: "https://api.linear.app/graphql",
|
|
11
|
-
requestPolicy: "cache-first",
|
|
12
|
-
exchanges: core_1.defaultExchanges,
|
|
13
|
-
fetch: node_fetch_1.default,
|
|
14
|
-
fetchOptions: {
|
|
15
|
-
headers: {
|
|
16
|
-
authorization: process.env.LINEAR_API_KEY,
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
|
-
});
|
|
20
|
-
async function getTeamKeys() {
|
|
21
|
-
const resp = await client
|
|
22
|
-
.query((0, core_1.gql) `
|
|
23
|
-
query FindTeamPrefixes {
|
|
24
|
-
teams {
|
|
25
|
-
nodes {
|
|
26
|
-
id
|
|
27
|
-
key
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
`, {})
|
|
32
|
-
.toPromise();
|
|
33
|
-
return resp.data?.teams.nodes.map((t) => t.key) ?? [];
|
|
34
|
-
}
|
|
35
|
-
exports.getTeamKeys = getTeamKeys;
|
|
36
|
-
const issueFragment = (0, core_1.gql) `
|
|
37
|
-
fragment Issue on Issue {
|
|
38
|
-
id
|
|
39
|
-
identifier
|
|
40
|
-
title
|
|
41
|
-
description
|
|
42
|
-
url
|
|
43
|
-
team {
|
|
44
|
-
id
|
|
45
|
-
key
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
`;
|
|
49
|
-
async function getIssueByKey(key) {
|
|
50
|
-
const components = key.split("-");
|
|
51
|
-
const teamKey = components[0];
|
|
52
|
-
if (teamKey?.length !== 3) {
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
const issueNumber = Number(components[1]);
|
|
56
|
-
if (!Number.isInteger(issueNumber)) {
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
const resp = await client
|
|
60
|
-
.query((0, core_1.gql) `
|
|
61
|
-
query GetIssueByKey($filter: IssueFilter!) {
|
|
62
|
-
issues(filter: $filter) {
|
|
63
|
-
nodes {
|
|
64
|
-
...Issue
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
${issueFragment}
|
|
69
|
-
`, {
|
|
70
|
-
filter: {
|
|
71
|
-
team: { key: { eqIgnoreCase: teamKey } },
|
|
72
|
-
number: { eq: issueNumber },
|
|
73
|
-
},
|
|
74
|
-
})
|
|
75
|
-
.toPromise();
|
|
76
|
-
return resp.data?.issues?.nodes[0];
|
|
77
|
-
}
|
|
78
|
-
exports.getIssueByKey = getIssueByKey;
|
|
79
|
-
async function findIssuesByTitle(teamKeys, issueTitle) {
|
|
80
|
-
const resp = await client
|
|
81
|
-
.query((0, core_1.gql) `
|
|
82
|
-
query FindIssuesByTitle(
|
|
83
|
-
$teamFilter: TeamFilter!
|
|
84
|
-
$issueFilter: IssueFilter!
|
|
85
|
-
) {
|
|
86
|
-
teams(filter: $teamFilter) {
|
|
87
|
-
nodes {
|
|
88
|
-
id
|
|
89
|
-
issues(filter: $issueFilter) {
|
|
90
|
-
nodes {
|
|
91
|
-
...Issue
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
${issueFragment}
|
|
98
|
-
`, {
|
|
99
|
-
teamFilter: {
|
|
100
|
-
or: teamKeys.map((tk) => ({
|
|
101
|
-
key: {
|
|
102
|
-
containsIgnoreCase: tk,
|
|
103
|
-
},
|
|
104
|
-
})),
|
|
105
|
-
},
|
|
106
|
-
issueFilter: {
|
|
107
|
-
title: {
|
|
108
|
-
containsIgnoreCase: issueTitle,
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
})
|
|
112
|
-
.toPromise();
|
|
113
|
-
return resp.data?.teams.nodes.flatMap((team) => team.issues.nodes);
|
|
114
|
-
}
|
|
115
|
-
exports.findIssuesByTitle = findIssuesByTitle;
|
package/dist/types.generated.js
DELETED