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.
Files changed (3) hide show
  1. package/README.md +10 -16
  2. package/dist/server.js +95 -40
  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,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 connection = (0, node_1.createConnection)(node_1.ProposedFeatures.all);
7
- 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);
8
8
  const teams = new Map();
9
9
  const issues = new Map();
10
- const issuePositions = new Map();
10
+ const positions = new Map();
11
11
  let client;
12
- connection.onInitialize(async () => {
12
+ conn.onInitialize(async () => {
13
13
  const apiKey = process.env.LINEAR_API_KEY;
14
14
  if (!apiKey) {
15
- 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.");
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
- const result = {
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
- connection.onCodeAction((params) => {
43
+ conn.onCodeAction((params) => {
42
44
  if (params.context.triggerKind === node_1.CodeActionTriggerKind.Invoked) {
43
- const textDocument = documents.get(params.textDocument.uri);
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
- // TODO: Implement command to create ticket
52
- 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
+ }
53
115
  }
54
116
  });
55
117
  let documentChangeTimeout;
56
- documents.onDidChangeContent((change) => {
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
- .flatMap((prefix) => {
63
- return Array.from(text.matchAll(new RegExp(`${prefix}-[0-9]*`, "g")));
64
- })
65
- .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) => {
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
- issuePositions.set(change.document.uri, documentPositions);
146
+ positions.set(change.document.uri, documentPositions);
83
147
  }, 200);
84
148
  });
85
- connection.onHover(async (params) => {
86
- const documentPositions = issuePositions.get(params.textDocument.uri);
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 = documents.get(params.textDocument.uri);
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
- connection.onCompletion(async (params) => {
178
+ conn.onCompletion(async (params) => {
115
179
  if (teams.size === 0) {
116
180
  return [];
117
181
  }
118
- const document = documents.get(params.textDocument.uri);
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
- connection.console.error(`LLS: Error searching issues: ${e}`);
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
- connection.languages.semanticTokens.on((params) => {
161
- const positions = issuePositions.get(params.textDocument.uri);
162
- if (!positions) {
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
- 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.
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 // token modifiers (none)
181
- );
236
+ 0);
182
237
  }
183
238
  return builder.build();
184
239
  });
185
- documents.listen(connection);
186
- 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.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
- }
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
  }