linear-ls 1.1.1 → 1.3.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/README.md +10 -16
- package/dist/server.js +176 -58
- package/package.json +28 -28
package/README.md
CHANGED
|
@@ -10,22 +10,14 @@ npm i -g linear-ls
|
|
|
10
10
|
|
|
11
11
|
### neovim
|
|
12
12
|
|
|
13
|
-
If you use `lspconfig`, add the following:
|
|
14
|
-
|
|
15
13
|
```lua
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
end,
|
|
24
|
-
},
|
|
25
|
-
}
|
|
26
|
-
end
|
|
27
|
-
lspconfig.lls.setup({
|
|
28
|
-
capabilities = capabilities,
|
|
14
|
+
vim.lsp.config("linear-ls", {
|
|
15
|
+
cmd = { "linear-ls", "--stdio" },
|
|
16
|
+
filetypes = { "typescript" },
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
vim.lsp.enable({
|
|
20
|
+
"linear-ls",
|
|
29
21
|
})
|
|
30
22
|
```
|
|
31
23
|
|
|
@@ -47,4 +39,6 @@ Typing team key, hyphen and search term (e.g. `EUC-thing`) triggers issue search
|
|
|
47
39
|
|
|
48
40
|
### Create Ticket from Text Selection
|
|
49
41
|
|
|
50
|
-
|
|
42
|
+
Select text in your editor and trigger the code action (e.g., `ga` in Neovim or `Cmd+.` in VS Code) to create a Linear issue. The selected text will be used as the issue title, and it will be replaced with a link to the created ticket in the format `[KEY-123](https://linear.app/...)`.
|
|
43
|
+
|
|
44
|
+
_Note: The server currently uses the first available team it has access to._
|
package/dist/server.js
CHANGED
|
@@ -3,17 +3,51 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
const sdk_1 = require("@linear/sdk");
|
|
4
4
|
const node_1 = require("vscode-languageserver/node");
|
|
5
5
|
const vscode_languageserver_textdocument_1 = require("vscode-languageserver-textdocument");
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
const documents = new node_1.TextDocuments(vscode_languageserver_textdocument_1.TextDocument);
|
|
6
|
+
const conn = (0, node_1.createConnection)(node_1.ProposedFeatures.all);
|
|
7
|
+
const docs = new node_1.TextDocuments(vscode_languageserver_textdocument_1.TextDocument);
|
|
9
8
|
const teams = new Map();
|
|
10
9
|
const issues = new Map();
|
|
11
|
-
const
|
|
10
|
+
const positions = new Map();
|
|
12
11
|
let client;
|
|
13
|
-
|
|
12
|
+
async function diagnostics(textDocument) {
|
|
13
|
+
if (!client) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const docPositions = positions.get(textDocument.uri);
|
|
17
|
+
if (!docPositions) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const diagnostics = [];
|
|
21
|
+
for (const p of docPositions) {
|
|
22
|
+
let issue = issues.get(p.issueKey);
|
|
23
|
+
if (!issue) {
|
|
24
|
+
issue = await client.issue(p.issueKey).catch(() => undefined);
|
|
25
|
+
if (issue)
|
|
26
|
+
issues.set(p.issueKey, issue);
|
|
27
|
+
}
|
|
28
|
+
if (issue?.completedAt) {
|
|
29
|
+
diagnostics.push({
|
|
30
|
+
severity: node_1.DiagnosticSeverity.Information,
|
|
31
|
+
range: { start: p.positionStart, end: p.positionEnd },
|
|
32
|
+
message: `${p.issueKey} is completed`,
|
|
33
|
+
source: "Linear",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
else if (issue?.archivedAt) {
|
|
37
|
+
diagnostics.push({
|
|
38
|
+
severity: node_1.DiagnosticSeverity.Information,
|
|
39
|
+
range: { start: p.positionStart, end: p.positionEnd },
|
|
40
|
+
message: `${p.issueKey} is archived`,
|
|
41
|
+
source: "Linear",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
conn.sendDiagnostics({ uri: textDocument.uri, diagnostics });
|
|
46
|
+
}
|
|
47
|
+
conn.onInitialize(async () => {
|
|
14
48
|
const apiKey = process.env.LINEAR_API_KEY;
|
|
15
49
|
if (!apiKey) {
|
|
16
|
-
|
|
50
|
+
conn.window.showWarningMessage("Linear API Key not found. Please set the LINEAR_API_KEY environment variable.");
|
|
17
51
|
return { capabilities: {} };
|
|
18
52
|
}
|
|
19
53
|
client = new sdk_1.LinearClient({ apiKey });
|
|
@@ -22,10 +56,13 @@ connection.onInitialize(async () => {
|
|
|
22
56
|
teams.set(t.key, t.id);
|
|
23
57
|
});
|
|
24
58
|
});
|
|
25
|
-
|
|
59
|
+
return {
|
|
26
60
|
capabilities: {
|
|
27
61
|
textDocumentSync: node_1.TextDocumentSyncKind.Incremental,
|
|
28
62
|
codeActionProvider: {},
|
|
63
|
+
executeCommandProvider: {
|
|
64
|
+
commands: ["linear-ls.createIssue", "linear-ls.openIssue"],
|
|
65
|
+
},
|
|
29
66
|
hoverProvider: {},
|
|
30
67
|
completionProvider: { resolveProvider: true },
|
|
31
68
|
semanticTokensProvider: {
|
|
@@ -37,33 +74,124 @@ connection.onInitialize(async () => {
|
|
|
37
74
|
},
|
|
38
75
|
},
|
|
39
76
|
};
|
|
40
|
-
return result;
|
|
41
77
|
});
|
|
42
|
-
|
|
78
|
+
conn.onCodeAction(async (params) => {
|
|
79
|
+
const actions = [];
|
|
43
80
|
if (params.context.triggerKind === node_1.CodeActionTriggerKind.Invoked) {
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
81
|
+
const doc = docs.get(params.textDocument.uri);
|
|
82
|
+
if (doc) {
|
|
83
|
+
const text = doc.getText(params.range);
|
|
84
|
+
if (text) {
|
|
85
|
+
actions.push({
|
|
86
|
+
title: "Create Issue from Selection",
|
|
87
|
+
kind: node_1.CodeActionKind.RefactorRewrite,
|
|
88
|
+
command: {
|
|
89
|
+
command: "linear-ls.createIssue",
|
|
90
|
+
title: "Create Issue",
|
|
91
|
+
arguments: [params.textDocument.uri, params.range, text.trim()],
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const docPositions = positions.get(params.textDocument.uri);
|
|
98
|
+
for (const p of docPositions ?? []) {
|
|
99
|
+
if (p.positionStart.line === params.range.start.line) {
|
|
100
|
+
if (params.range.start.character >= p.positionStart.character &&
|
|
101
|
+
params.range.start.character <= p.positionEnd.character) {
|
|
102
|
+
let issue = issues.get(p.issueKey);
|
|
103
|
+
if (!issue) {
|
|
104
|
+
issue = await client.issue(p.issueKey).catch(() => undefined);
|
|
105
|
+
if (issue)
|
|
106
|
+
issues.set(p.issueKey, issue);
|
|
107
|
+
}
|
|
108
|
+
if (issue?.url) {
|
|
109
|
+
actions.push({
|
|
110
|
+
title: `Open ${p.issueKey} in Linear`,
|
|
111
|
+
kind: node_1.CodeActionKind.Empty,
|
|
112
|
+
command: {
|
|
113
|
+
command: "linear-ls.openIssue",
|
|
114
|
+
title: "Open in Linear",
|
|
115
|
+
arguments: [issue.url],
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return actions;
|
|
123
|
+
});
|
|
124
|
+
conn.onExecuteCommand(async (params) => {
|
|
125
|
+
if (params.command === "linear-ls.openIssue") {
|
|
126
|
+
const [url] = params.arguments || [];
|
|
127
|
+
if (url) {
|
|
128
|
+
conn.window.showDocument({ uri: url, external: true });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (params.command === "linear-ls.createTicket") {
|
|
132
|
+
const [uri, range, title] = params.arguments || [];
|
|
133
|
+
if (!uri || !range || !title) {
|
|
46
134
|
return;
|
|
47
135
|
}
|
|
48
|
-
|
|
49
|
-
|
|
136
|
+
if (teams.size === 0) {
|
|
137
|
+
conn.window.showErrorMessage("No teams found. Please ensure you have access to at least one Linear team.");
|
|
50
138
|
return;
|
|
51
139
|
}
|
|
52
|
-
|
|
53
|
-
|
|
140
|
+
let teamId;
|
|
141
|
+
if (teams.size === 1) {
|
|
142
|
+
teamId = teams.values().next().value;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
const teamItems = Array.from(teams.keys()).map((key) => ({
|
|
146
|
+
title: key,
|
|
147
|
+
}));
|
|
148
|
+
const selected = await conn.window.showInformationMessage("Select a team for the new ticket:", ...teamItems);
|
|
149
|
+
if (selected) {
|
|
150
|
+
teamId = teams.get(selected.title);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!teamId) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const issue = await client.createIssue({ teamId, title });
|
|
158
|
+
const createdIssue = await issue.issue;
|
|
159
|
+
if (createdIssue) {
|
|
160
|
+
const identifier = createdIssue.identifier;
|
|
161
|
+
const url = createdIssue.url;
|
|
162
|
+
const link = `[${identifier}](${url})`;
|
|
163
|
+
const edit = {
|
|
164
|
+
changes: {
|
|
165
|
+
[uri]: [
|
|
166
|
+
{
|
|
167
|
+
range: range,
|
|
168
|
+
newText: link,
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
await conn.workspace.applyEdit(edit);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
conn.console.error(`Error creating issue: ${e}`);
|
|
178
|
+
conn.window.showErrorMessage(`Failed to create Linear issue: ${e}`);
|
|
179
|
+
}
|
|
54
180
|
}
|
|
55
181
|
});
|
|
56
182
|
let documentChangeTimeout;
|
|
57
|
-
|
|
183
|
+
docs.onDidChangeContent((change) => {
|
|
58
184
|
clearTimeout(documentChangeTimeout);
|
|
59
|
-
documentChangeTimeout = setTimeout(() => {
|
|
185
|
+
documentChangeTimeout = setTimeout(async () => {
|
|
60
186
|
const text = change.document.getText();
|
|
61
187
|
const documentPositions = [];
|
|
62
|
-
Array.from(teams.keys())
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
188
|
+
const teamKeys = Array.from(teams.keys());
|
|
189
|
+
if (teamKeys.length === 0) {
|
|
190
|
+
positions.set(change.document.uri, []);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const regexp = new RegExp(`(${teamKeys.join("|")})-[0-9]*`, "g");
|
|
194
|
+
Array.from(text.matchAll(regexp)).forEach((m) => {
|
|
67
195
|
const issueKey = m[0];
|
|
68
196
|
const positionStart = change.document.positionAt(m.index ?? 0);
|
|
69
197
|
const positionEnd = change.document.positionAt(issueKey.length + (m.index ?? 0));
|
|
@@ -80,43 +208,42 @@ documents.onDidChangeContent((change) => {
|
|
|
80
208
|
offsetEnd: change.document.offsetAt(positionEnd),
|
|
81
209
|
});
|
|
82
210
|
});
|
|
83
|
-
|
|
84
|
-
|
|
211
|
+
positions.set(change.document.uri, documentPositions);
|
|
212
|
+
await diagnostics(change.document);
|
|
213
|
+
}, 1000);
|
|
85
214
|
});
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
if (!
|
|
215
|
+
conn.onHover(async (params) => {
|
|
216
|
+
const docPositions = positions.get(params.textDocument.uri);
|
|
217
|
+
if (!docPositions) {
|
|
89
218
|
return;
|
|
90
219
|
}
|
|
91
|
-
const
|
|
92
|
-
if (!
|
|
220
|
+
const doc = docs.get(params.textDocument.uri);
|
|
221
|
+
if (!doc) {
|
|
93
222
|
return;
|
|
94
223
|
}
|
|
95
|
-
const cursorOffset =
|
|
96
|
-
const
|
|
97
|
-
if (!
|
|
224
|
+
const cursorOffset = doc.offsetAt(params.position);
|
|
225
|
+
const pos = docPositions.find((dp) => dp.offsetStart <= cursorOffset && dp.offsetEnd > cursorOffset);
|
|
226
|
+
if (!pos) {
|
|
98
227
|
return;
|
|
99
228
|
}
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
229
|
+
let issue = issues.get(pos.issueKey);
|
|
230
|
+
if (!issue) {
|
|
231
|
+
issue = await client.issue(pos.issueKey).catch(() => undefined);
|
|
232
|
+
if (issue)
|
|
233
|
+
issues.set(pos.issueKey, issue);
|
|
105
234
|
}
|
|
106
|
-
const issue = await client.issue(targetIssue.issueKey);
|
|
107
235
|
if (!issue) {
|
|
108
236
|
return;
|
|
109
237
|
}
|
|
110
|
-
issues.set(targetIssue.issueKey, issue);
|
|
111
238
|
return {
|
|
112
239
|
contents: issue.description ?? "Not available",
|
|
113
240
|
};
|
|
114
241
|
});
|
|
115
|
-
|
|
242
|
+
conn.onCompletion(async (params) => {
|
|
116
243
|
if (teams.size === 0) {
|
|
117
244
|
return [];
|
|
118
245
|
}
|
|
119
|
-
const document =
|
|
246
|
+
const document = docs.get(params.textDocument.uri);
|
|
120
247
|
if (!document) {
|
|
121
248
|
return [];
|
|
122
249
|
}
|
|
@@ -141,7 +268,7 @@ connection.onCompletion(async (params) => {
|
|
|
141
268
|
});
|
|
142
269
|
}
|
|
143
270
|
catch (e) {
|
|
144
|
-
|
|
271
|
+
conn.console.error(`LLS: Error searching issues: ${e}`);
|
|
145
272
|
return [];
|
|
146
273
|
}
|
|
147
274
|
return issues.nodes.map((i) => {
|
|
@@ -158,30 +285,21 @@ connection.onCompletion(async (params) => {
|
|
|
158
285
|
};
|
|
159
286
|
});
|
|
160
287
|
});
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
if (!
|
|
288
|
+
conn.languages.semanticTokens.on((params) => {
|
|
289
|
+
const docPositions = positions.get(params.textDocument.uri);
|
|
290
|
+
if (!docPositions) {
|
|
164
291
|
return { data: [] };
|
|
165
292
|
}
|
|
166
293
|
const builder = new node_1.SemanticTokensBuilder();
|
|
167
|
-
|
|
168
|
-
if (a.positionStart.line !== b.positionStart.line) {
|
|
169
|
-
return a.positionStart.line - b.positionStart.line;
|
|
170
|
-
}
|
|
171
|
-
return a.positionStart.character - b.positionStart.character;
|
|
172
|
-
});
|
|
173
|
-
for (const p of positions) {
|
|
174
|
-
// Semantic tokens are assumed to be single-line. If a token ever spans multiple
|
|
175
|
-
// lines, its length must be calculated differently. For now, we ignore such ranges.
|
|
294
|
+
for (const p of docPositions) {
|
|
176
295
|
if (p.positionStart.line !== p.positionEnd.line) {
|
|
177
296
|
continue;
|
|
178
297
|
}
|
|
179
298
|
const length = p.positionEnd.character - p.positionStart.character;
|
|
180
299
|
builder.push(p.positionStart.line, p.positionStart.character, length, 0, // token type index (0 = variable, as defined in legend)
|
|
181
|
-
0
|
|
182
|
-
);
|
|
300
|
+
0);
|
|
183
301
|
}
|
|
184
302
|
return builder.build();
|
|
185
303
|
});
|
|
186
|
-
|
|
187
|
-
|
|
304
|
+
docs.listen(conn);
|
|
305
|
+
conn.listen();
|
package/package.json
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
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
|
-
|
|
2
|
+
"name": "linear-ls",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "Linear Language Server",
|
|
5
|
+
"bin": "index.js",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"lsp",
|
|
9
|
+
"linear",
|
|
10
|
+
"language",
|
|
11
|
+
"productivity"
|
|
12
|
+
],
|
|
13
|
+
"repository": "https://github.com/wilhelmeek/linear-ls",
|
|
14
|
+
"author": "wilhelm <helm@hey.com>",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"files": [
|
|
17
|
+
"dist/*.js",
|
|
18
|
+
"index.js"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@linear/sdk": "^68.1.0",
|
|
22
|
+
"vscode-languageserver": "^9.0.1",
|
|
23
|
+
"vscode-languageserver-textdocument": "^1.0.12"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@biomejs/biome": "^2.3.10",
|
|
27
|
+
"@types/node": "^23",
|
|
28
|
+
"typescript": "^5.9.3"
|
|
29
|
+
}
|
|
30
30
|
}
|