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.
Files changed (3) hide show
  1. package/README.md +10 -16
  2. package/dist/server.js +95 -41
  3. 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
- if not lspconfigconfigs.linear_ls then
17
- lspconfigconfigs.linear_ls = {
18
- default_config = {
19
- cmd = { "linear-ls", "--stdio" },
20
- filetypes = { "typescript" },
21
- root_dir = function(fname)
22
- return lspconfig.util.find_git_ancestor(fname)
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
- TODO
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
- // ENG-100
7
- const connection = (0, node_1.createConnection)(node_1.ProposedFeatures.all);
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 issuePositions = new Map();
10
+ const positions = new Map();
12
11
  let client;
13
- connection.onInitialize(async () => {
12
+ conn.onInitialize(async () => {
14
13
  const apiKey = process.env.LINEAR_API_KEY;
15
14
  if (!apiKey) {
16
- connection.window.showWarningMessage("Linear API Key not found. Please set the LINEAR_API_KEY environment variable.");
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
- const result = {
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
- connection.onCodeAction((params) => {
43
+ conn.onCodeAction((params) => {
43
44
  if (params.context.triggerKind === node_1.CodeActionTriggerKind.Invoked) {
44
- const textDocument = documents.get(params.textDocument.uri);
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
- // TODO: Implement command to create ticket
53
- return [{ title: "Create Ticket" }];
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
- documents.onDidChangeContent((change) => {
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
- .flatMap((prefix) => {
64
- return Array.from(text.matchAll(new RegExp(`${prefix}-[0-9]*`, "g")));
65
- })
66
- .forEach((m) => {
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
- issuePositions.set(change.document.uri, documentPositions);
146
+ positions.set(change.document.uri, documentPositions);
84
147
  }, 200);
85
148
  });
86
- connection.onHover(async (params) => {
87
- const documentPositions = issuePositions.get(params.textDocument.uri);
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 = documents.get(params.textDocument.uri);
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
- connection.onCompletion(async (params) => {
178
+ conn.onCompletion(async (params) => {
116
179
  if (teams.size === 0) {
117
180
  return [];
118
181
  }
119
- const document = documents.get(params.textDocument.uri);
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
- connection.console.error(`LLS: Error searching issues: ${e}`);
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
- connection.languages.semanticTokens.on((params) => {
162
- const positions = issuePositions.get(params.textDocument.uri);
163
- if (!positions) {
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
- positions.sort((a, b) => {
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 // token modifiers (none)
182
- );
236
+ 0);
183
237
  }
184
238
  return builder.build();
185
239
  });
186
- documents.listen(connection);
187
- connection.listen();
240
+ docs.listen(conn);
241
+ conn.listen();
package/package.json CHANGED
@@ -1,30 +1,30 @@
1
1
  {
2
- "name": "linear-ls",
3
- "version": "1.1.1",
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
- }
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
  }