linear-ls 1.1.1 → 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 -41
- 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,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
|
-
|
|
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
|
+
conn.onInitialize(async () => {
|
|
14
13
|
const apiKey = process.env.LINEAR_API_KEY;
|
|
15
14
|
if (!apiKey) {
|
|
16
|
-
|
|
15
|
+
conn.window.showWarningMessage("Linear API Key not found. Please set the LINEAR_API_KEY environment variable.");
|
|
17
16
|
return { capabilities: {} };
|
|
18
17
|
}
|
|
19
18
|
client = new sdk_1.LinearClient({ apiKey });
|
|
@@ -22,10 +21,13 @@ connection.onInitialize(async () => {
|
|
|
22
21
|
teams.set(t.key, t.id);
|
|
23
22
|
});
|
|
24
23
|
});
|
|
25
|
-
|
|
24
|
+
return {
|
|
26
25
|
capabilities: {
|
|
27
26
|
textDocumentSync: node_1.TextDocumentSyncKind.Incremental,
|
|
28
27
|
codeActionProvider: {},
|
|
28
|
+
executeCommandProvider: {
|
|
29
|
+
commands: ["linear-ls.createTicket"],
|
|
30
|
+
},
|
|
29
31
|
hoverProvider: {},
|
|
30
32
|
completionProvider: { resolveProvider: true },
|
|
31
33
|
semanticTokensProvider: {
|
|
@@ -37,11 +39,10 @@ connection.onInitialize(async () => {
|
|
|
37
39
|
},
|
|
38
40
|
},
|
|
39
41
|
};
|
|
40
|
-
return result;
|
|
41
42
|
});
|
|
42
|
-
|
|
43
|
+
conn.onCodeAction((params) => {
|
|
43
44
|
if (params.context.triggerKind === node_1.CodeActionTriggerKind.Invoked) {
|
|
44
|
-
const textDocument =
|
|
45
|
+
const textDocument = docs.get(params.textDocument.uri);
|
|
45
46
|
if (!textDocument) {
|
|
46
47
|
return;
|
|
47
48
|
}
|
|
@@ -49,21 +50,83 @@ connection.onCodeAction((params) => {
|
|
|
49
50
|
if (!text) {
|
|
50
51
|
return;
|
|
51
52
|
}
|
|
52
|
-
|
|
53
|
-
|
|
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
|
+
}
|
|
54
115
|
}
|
|
55
116
|
});
|
|
56
117
|
let documentChangeTimeout;
|
|
57
|
-
|
|
118
|
+
docs.onDidChangeContent((change) => {
|
|
58
119
|
clearTimeout(documentChangeTimeout);
|
|
59
120
|
documentChangeTimeout = setTimeout(() => {
|
|
60
121
|
const text = change.document.getText();
|
|
61
122
|
const documentPositions = [];
|
|
62
|
-
Array.from(teams.keys())
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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) => {
|
|
67
130
|
const issueKey = m[0];
|
|
68
131
|
const positionStart = change.document.positionAt(m.index ?? 0);
|
|
69
132
|
const positionEnd = change.document.positionAt(issueKey.length + (m.index ?? 0));
|
|
@@ -80,15 +143,15 @@ documents.onDidChangeContent((change) => {
|
|
|
80
143
|
offsetEnd: change.document.offsetAt(positionEnd),
|
|
81
144
|
});
|
|
82
145
|
});
|
|
83
|
-
|
|
146
|
+
positions.set(change.document.uri, documentPositions);
|
|
84
147
|
}, 200);
|
|
85
148
|
});
|
|
86
|
-
|
|
87
|
-
const documentPositions =
|
|
149
|
+
conn.onHover(async (params) => {
|
|
150
|
+
const documentPositions = positions.get(params.textDocument.uri);
|
|
88
151
|
if (!documentPositions) {
|
|
89
152
|
return;
|
|
90
153
|
}
|
|
91
|
-
const textDocument =
|
|
154
|
+
const textDocument = docs.get(params.textDocument.uri);
|
|
92
155
|
if (!textDocument) {
|
|
93
156
|
return;
|
|
94
157
|
}
|
|
@@ -112,11 +175,11 @@ connection.onHover(async (params) => {
|
|
|
112
175
|
contents: issue.description ?? "Not available",
|
|
113
176
|
};
|
|
114
177
|
});
|
|
115
|
-
|
|
178
|
+
conn.onCompletion(async (params) => {
|
|
116
179
|
if (teams.size === 0) {
|
|
117
180
|
return [];
|
|
118
181
|
}
|
|
119
|
-
const document =
|
|
182
|
+
const document = docs.get(params.textDocument.uri);
|
|
120
183
|
if (!document) {
|
|
121
184
|
return [];
|
|
122
185
|
}
|
|
@@ -141,7 +204,7 @@ connection.onCompletion(async (params) => {
|
|
|
141
204
|
});
|
|
142
205
|
}
|
|
143
206
|
catch (e) {
|
|
144
|
-
|
|
207
|
+
conn.console.error(`LLS: Error searching issues: ${e}`);
|
|
145
208
|
return [];
|
|
146
209
|
}
|
|
147
210
|
return issues.nodes.map((i) => {
|
|
@@ -158,30 +221,21 @@ connection.onCompletion(async (params) => {
|
|
|
158
221
|
};
|
|
159
222
|
});
|
|
160
223
|
});
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
if (!
|
|
224
|
+
conn.languages.semanticTokens.on((params) => {
|
|
225
|
+
const docPositions = positions.get(params.textDocument.uri);
|
|
226
|
+
if (!docPositions) {
|
|
164
227
|
return { data: [] };
|
|
165
228
|
}
|
|
166
229
|
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.
|
|
230
|
+
for (const p of docPositions) {
|
|
176
231
|
if (p.positionStart.line !== p.positionEnd.line) {
|
|
177
232
|
continue;
|
|
178
233
|
}
|
|
179
234
|
const length = p.positionEnd.character - p.positionStart.character;
|
|
180
235
|
builder.push(p.positionStart.line, p.positionStart.character, length, 0, // token type index (0 = variable, as defined in legend)
|
|
181
|
-
0
|
|
182
|
-
);
|
|
236
|
+
0);
|
|
183
237
|
}
|
|
184
238
|
return builder.build();
|
|
185
239
|
});
|
|
186
|
-
|
|
187
|
-
|
|
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
|
}
|