linear-ls 1.1.0 → 1.2.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 +95 -40
- 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,16 +3,16 @@ 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
|
-
const
|
|
7
|
-
const
|
|
6
|
+
const conn = (0, node_1.createConnection)(node_1.ProposedFeatures.all);
|
|
7
|
+
const docs = new node_1.TextDocuments(vscode_languageserver_textdocument_1.TextDocument);
|
|
8
8
|
const teams = new Map();
|
|
9
9
|
const issues = new Map();
|
|
10
|
-
const
|
|
10
|
+
const positions = new Map();
|
|
11
11
|
let client;
|
|
12
|
-
|
|
12
|
+
conn.onInitialize(async () => {
|
|
13
13
|
const apiKey = process.env.LINEAR_API_KEY;
|
|
14
14
|
if (!apiKey) {
|
|
15
|
-
|
|
15
|
+
conn.window.showWarningMessage("Linear API Key not found. Please set the LINEAR_API_KEY environment variable.");
|
|
16
16
|
return { capabilities: {} };
|
|
17
17
|
}
|
|
18
18
|
client = new sdk_1.LinearClient({ apiKey });
|
|
@@ -21,10 +21,13 @@ connection.onInitialize(async () => {
|
|
|
21
21
|
teams.set(t.key, t.id);
|
|
22
22
|
});
|
|
23
23
|
});
|
|
24
|
-
|
|
24
|
+
return {
|
|
25
25
|
capabilities: {
|
|
26
26
|
textDocumentSync: node_1.TextDocumentSyncKind.Incremental,
|
|
27
27
|
codeActionProvider: {},
|
|
28
|
+
executeCommandProvider: {
|
|
29
|
+
commands: ["linear-ls.createTicket"],
|
|
30
|
+
},
|
|
28
31
|
hoverProvider: {},
|
|
29
32
|
completionProvider: { resolveProvider: true },
|
|
30
33
|
semanticTokensProvider: {
|
|
@@ -36,11 +39,10 @@ connection.onInitialize(async () => {
|
|
|
36
39
|
},
|
|
37
40
|
},
|
|
38
41
|
};
|
|
39
|
-
return result;
|
|
40
42
|
});
|
|
41
|
-
|
|
43
|
+
conn.onCodeAction((params) => {
|
|
42
44
|
if (params.context.triggerKind === node_1.CodeActionTriggerKind.Invoked) {
|
|
43
|
-
const textDocument =
|
|
45
|
+
const textDocument = docs.get(params.textDocument.uri);
|
|
44
46
|
if (!textDocument) {
|
|
45
47
|
return;
|
|
46
48
|
}
|
|
@@ -48,21 +50,83 @@ connection.onCodeAction((params) => {
|
|
|
48
50
|
if (!text) {
|
|
49
51
|
return;
|
|
50
52
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
const action = {
|
|
54
|
+
title: "Create Ticket from Selection",
|
|
55
|
+
kind: node_1.CodeActionKind.RefactorRewrite,
|
|
56
|
+
command: {
|
|
57
|
+
command: "linear-ls.createTicket",
|
|
58
|
+
title: "Create Ticket",
|
|
59
|
+
arguments: [params.textDocument.uri, params.range, text.trim()],
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
return [action];
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
conn.onExecuteCommand(async (params) => {
|
|
66
|
+
if (params.command === "linear-ls.createTicket") {
|
|
67
|
+
const [uri, range, title] = params.arguments || [];
|
|
68
|
+
if (!uri || !range || !title) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (teams.size === 0) {
|
|
72
|
+
conn.window.showErrorMessage("No teams found. Please ensure you have access to at least one Linear team.");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
let teamId;
|
|
76
|
+
if (teams.size === 1) {
|
|
77
|
+
teamId = teams.values().next().value;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
const teamItems = Array.from(teams.keys()).map((key) => ({
|
|
81
|
+
title: key,
|
|
82
|
+
}));
|
|
83
|
+
const selected = await conn.window.showInformationMessage("Select a team for the new ticket:", ...teamItems);
|
|
84
|
+
if (selected) {
|
|
85
|
+
teamId = teams.get(selected.title);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (!teamId) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const issue = await client.createIssue({ teamId, title });
|
|
93
|
+
const createdIssue = await issue.issue;
|
|
94
|
+
if (createdIssue) {
|
|
95
|
+
const identifier = createdIssue.identifier;
|
|
96
|
+
const url = createdIssue.url;
|
|
97
|
+
const link = `[${identifier}](${url})`;
|
|
98
|
+
const edit = {
|
|
99
|
+
changes: {
|
|
100
|
+
[uri]: [
|
|
101
|
+
{
|
|
102
|
+
range: range,
|
|
103
|
+
newText: link,
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
await conn.workspace.applyEdit(edit);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
conn.console.error(`Error creating issue: ${e}`);
|
|
113
|
+
conn.window.showErrorMessage(`Failed to create Linear issue: ${e}`);
|
|
114
|
+
}
|
|
53
115
|
}
|
|
54
116
|
});
|
|
55
117
|
let documentChangeTimeout;
|
|
56
|
-
|
|
118
|
+
docs.onDidChangeContent((change) => {
|
|
57
119
|
clearTimeout(documentChangeTimeout);
|
|
58
120
|
documentChangeTimeout = setTimeout(() => {
|
|
59
121
|
const text = change.document.getText();
|
|
60
122
|
const documentPositions = [];
|
|
61
|
-
Array.from(teams.keys())
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
123
|
+
const teamKeys = Array.from(teams.keys());
|
|
124
|
+
if (teamKeys.length === 0) {
|
|
125
|
+
positions.set(change.document.uri, []);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const regexp = new RegExp(`(${teamKeys.join("|")})-[0-9]*`, "g");
|
|
129
|
+
Array.from(text.matchAll(regexp)).forEach((m) => {
|
|
66
130
|
const issueKey = m[0];
|
|
67
131
|
const positionStart = change.document.positionAt(m.index ?? 0);
|
|
68
132
|
const positionEnd = change.document.positionAt(issueKey.length + (m.index ?? 0));
|
|
@@ -79,15 +143,15 @@ documents.onDidChangeContent((change) => {
|
|
|
79
143
|
offsetEnd: change.document.offsetAt(positionEnd),
|
|
80
144
|
});
|
|
81
145
|
});
|
|
82
|
-
|
|
146
|
+
positions.set(change.document.uri, documentPositions);
|
|
83
147
|
}, 200);
|
|
84
148
|
});
|
|
85
|
-
|
|
86
|
-
const documentPositions =
|
|
149
|
+
conn.onHover(async (params) => {
|
|
150
|
+
const documentPositions = positions.get(params.textDocument.uri);
|
|
87
151
|
if (!documentPositions) {
|
|
88
152
|
return;
|
|
89
153
|
}
|
|
90
|
-
const textDocument =
|
|
154
|
+
const textDocument = docs.get(params.textDocument.uri);
|
|
91
155
|
if (!textDocument) {
|
|
92
156
|
return;
|
|
93
157
|
}
|
|
@@ -111,11 +175,11 @@ connection.onHover(async (params) => {
|
|
|
111
175
|
contents: issue.description ?? "Not available",
|
|
112
176
|
};
|
|
113
177
|
});
|
|
114
|
-
|
|
178
|
+
conn.onCompletion(async (params) => {
|
|
115
179
|
if (teams.size === 0) {
|
|
116
180
|
return [];
|
|
117
181
|
}
|
|
118
|
-
const document =
|
|
182
|
+
const document = docs.get(params.textDocument.uri);
|
|
119
183
|
if (!document) {
|
|
120
184
|
return [];
|
|
121
185
|
}
|
|
@@ -140,7 +204,7 @@ connection.onCompletion(async (params) => {
|
|
|
140
204
|
});
|
|
141
205
|
}
|
|
142
206
|
catch (e) {
|
|
143
|
-
|
|
207
|
+
conn.console.error(`LLS: Error searching issues: ${e}`);
|
|
144
208
|
return [];
|
|
145
209
|
}
|
|
146
210
|
return issues.nodes.map((i) => {
|
|
@@ -157,30 +221,21 @@ connection.onCompletion(async (params) => {
|
|
|
157
221
|
};
|
|
158
222
|
});
|
|
159
223
|
});
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
if (!
|
|
224
|
+
conn.languages.semanticTokens.on((params) => {
|
|
225
|
+
const docPositions = positions.get(params.textDocument.uri);
|
|
226
|
+
if (!docPositions) {
|
|
163
227
|
return { data: [] };
|
|
164
228
|
}
|
|
165
229
|
const builder = new node_1.SemanticTokensBuilder();
|
|
166
|
-
|
|
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.
|
|
230
|
+
for (const p of docPositions) {
|
|
175
231
|
if (p.positionStart.line !== p.positionEnd.line) {
|
|
176
232
|
continue;
|
|
177
233
|
}
|
|
178
234
|
const length = p.positionEnd.character - p.positionStart.character;
|
|
179
235
|
builder.push(p.positionStart.line, p.positionStart.character, length, 0, // token type index (0 = variable, as defined in legend)
|
|
180
|
-
0
|
|
181
|
-
);
|
|
236
|
+
0);
|
|
182
237
|
}
|
|
183
238
|
return builder.build();
|
|
184
239
|
});
|
|
185
|
-
|
|
186
|
-
|
|
240
|
+
docs.listen(conn);
|
|
241
|
+
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.2.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
|
}
|