linear-ls 1.0.4 → 1.1.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 CHANGED
@@ -13,8 +13,8 @@ npm i -g linear-ls
13
13
  If you use `lspconfig`, add the following:
14
14
 
15
15
  ```lua
16
- if not lspconfigconfigs.lls then
17
- lspconfigconfigs.lls = {
16
+ if not lspconfigconfigs.linear_ls then
17
+ lspconfigconfigs.linear_ls = {
18
18
  default_config = {
19
19
  cmd = { "linear-ls", "--stdio" },
20
20
  filetypes = { "typescript" },
package/dist/server.js CHANGED
@@ -1,17 +1,24 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const sdk_1 = require("@linear/sdk");
3
4
  const node_1 = require("vscode-languageserver/node");
4
5
  const vscode_languageserver_textdocument_1 = require("vscode-languageserver-textdocument");
5
- const linear_1 = require("./linear");
6
6
  const connection = (0, node_1.createConnection)(node_1.ProposedFeatures.all);
7
7
  const documents = new node_1.TextDocuments(vscode_languageserver_textdocument_1.TextDocument);
8
- const teamKeys = new Set();
8
+ const teams = new Map();
9
9
  const issues = new Map();
10
10
  const issuePositions = new Map();
11
+ let client;
11
12
  connection.onInitialize(async () => {
12
- await (0, linear_1.getTeamKeys)().then((keys) => {
13
- keys.map((k) => {
14
- teamKeys.add(k);
13
+ const apiKey = process.env.LINEAR_API_KEY;
14
+ if (!apiKey) {
15
+ connection.window.showWarningMessage("Linear API Key not found. Please set the LINEAR_API_KEY environment variable.");
16
+ return { capabilities: {} };
17
+ }
18
+ client = new sdk_1.LinearClient({ apiKey });
19
+ await client.teams().then((ts) => {
20
+ ts.nodes.forEach((t) => {
21
+ teams.set(t.key, t.id);
15
22
  });
16
23
  });
17
24
  const result = {
@@ -20,6 +27,13 @@ connection.onInitialize(async () => {
20
27
  codeActionProvider: {},
21
28
  hoverProvider: {},
22
29
  completionProvider: { resolveProvider: true },
30
+ semanticTokensProvider: {
31
+ legend: {
32
+ tokenTypes: [node_1.SemanticTokenTypes.variable],
33
+ tokenModifiers: [],
34
+ },
35
+ full: true,
36
+ },
23
37
  },
24
38
  };
25
39
  return result;
@@ -38,31 +52,35 @@ connection.onCodeAction((params) => {
38
52
  return [{ title: "Create Ticket" }];
39
53
  }
40
54
  });
55
+ let documentChangeTimeout;
41
56
  documents.onDidChangeContent((change) => {
42
- const text = change.document.getText();
43
- const documentPositions = [];
44
- Array.from(teamKeys)
45
- .flatMap((prefix) => {
46
- return Array.from(text.matchAll(new RegExp(`${prefix}-[0-9]*`, "g")));
47
- })
48
- .forEach((m) => {
49
- const issueKey = m[0];
50
- const positionStart = change.document.positionAt(m.index ?? 0);
51
- const positionEnd = change.document.positionAt(issueKey.length + (m.index ?? 0));
52
- // Write down that we've seen the issue, but don't
53
- // fetch the definition just yet.
54
- if (!issues.has(issueKey)) {
55
- issues.set(issueKey, undefined);
56
- }
57
- documentPositions.push({
58
- issueKey,
59
- positionStart,
60
- positionEnd,
61
- offsetStart: change.document.offsetAt(positionStart),
62
- offsetEnd: change.document.offsetAt(positionEnd),
57
+ clearTimeout(documentChangeTimeout);
58
+ documentChangeTimeout = setTimeout(() => {
59
+ const text = change.document.getText();
60
+ 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) => {
66
+ const issueKey = m[0];
67
+ const positionStart = change.document.positionAt(m.index ?? 0);
68
+ const positionEnd = change.document.positionAt(issueKey.length + (m.index ?? 0));
69
+ // Write down that we've seen the issue, but don't
70
+ // fetch the definition just yet.
71
+ if (!issues.has(issueKey)) {
72
+ issues.set(issueKey, undefined);
73
+ }
74
+ documentPositions.push({
75
+ issueKey,
76
+ positionStart,
77
+ positionEnd,
78
+ offsetStart: change.document.offsetAt(positionStart),
79
+ offsetEnd: change.document.offsetAt(positionEnd),
80
+ });
63
81
  });
64
- });
65
- issuePositions.set(change.document.uri, documentPositions);
82
+ issuePositions.set(change.document.uri, documentPositions);
83
+ }, 200);
66
84
  });
67
85
  connection.onHover(async (params) => {
68
86
  const documentPositions = issuePositions.get(params.textDocument.uri);
@@ -78,16 +96,23 @@ connection.onHover(async (params) => {
78
96
  if (!targetIssue) {
79
97
  return;
80
98
  }
81
- const issue = await (0, linear_1.getIssueByKey)(targetIssue.issueKey);
99
+ const issueFromCache = issues.get(targetIssue.issueKey);
100
+ if (issueFromCache) {
101
+ return {
102
+ contents: issueFromCache.description ?? "Not available",
103
+ };
104
+ }
105
+ const issue = await client.issue(targetIssue.issueKey);
82
106
  if (!issue) {
83
107
  return;
84
108
  }
109
+ issues.set(targetIssue.issueKey, issue);
85
110
  return {
86
111
  contents: issue.description ?? "Not available",
87
112
  };
88
113
  });
89
114
  connection.onCompletion(async (params) => {
90
- if (teamKeys.size === 0) {
115
+ if (teams.size === 0) {
91
116
  return [];
92
117
  }
93
118
  const document = documents.get(params.textDocument.uri);
@@ -98,7 +123,7 @@ connection.onCompletion(async (params) => {
98
123
  start: { line: params.position.line, character: 0 },
99
124
  end: { line: params.position.line, character: params.position.character },
100
125
  });
101
- const lastMatch = Array.from(teamKeys)
126
+ const lastMatch = Array.from(teams.keys())
102
127
  .flatMap((prefix) => Array.from(lineToPosition.matchAll(new RegExp(`(${prefix})-(.*)`, "g"))))
103
128
  .at(-1);
104
129
  if (!lastMatch) {
@@ -108,24 +133,54 @@ connection.onCompletion(async (params) => {
108
133
  if (!teamKey || !searchString) {
109
134
  return [];
110
135
  }
111
- const issues = await (0, linear_1.findIssuesByTitle)([teamKey], searchString);
112
- if (issues === undefined) {
136
+ let issues;
137
+ try {
138
+ issues = await client.searchIssues(searchString, {
139
+ teamId: teams.get(teamKey),
140
+ });
141
+ }
142
+ catch (e) {
143
+ connection.console.error(`LLS: Error searching issues: ${e}`);
113
144
  return [];
114
145
  }
115
- return issues.map((issue) => {
116
- let label = `${issue.identifier}: ${issue.title}`;
146
+ return issues.nodes.map((i) => {
147
+ let label = `${i.identifier}: ${i.title}`;
117
148
  if (label.length > 60) {
118
- label = label.substring(0, 60) + "...";
149
+ label = `${label.substring(0, 60)}...`;
119
150
  }
120
151
  return {
121
- data: issue,
122
- detail: issue.description ?? "Not available",
123
- insertText: `[${issue.identifier}](${issue.url})`,
124
- filterText: `${issue.team.key}-${issue.title}`,
152
+ data: i,
153
+ detail: i.description ?? "Not available",
154
+ insertText: `[${i.identifier}](${i.url})`,
125
155
  label,
126
156
  kind: node_1.CompletionItemKind.Reference,
127
157
  };
128
158
  });
129
159
  });
160
+ connection.languages.semanticTokens.on((params) => {
161
+ const positions = issuePositions.get(params.textDocument.uri);
162
+ if (!positions) {
163
+ return { data: [] };
164
+ }
165
+ 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.
175
+ if (p.positionStart.line !== p.positionEnd.line) {
176
+ continue;
177
+ }
178
+ const length = p.positionEnd.character - p.positionStart.character;
179
+ 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
+ );
182
+ }
183
+ return builder.build();
184
+ });
130
185
  documents.listen(connection);
131
186
  connection.listen();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linear-ls",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "description": "Linear Language Server",
5
5
  "bin": "index.js",
6
6
  "main": "index.js",
@@ -10,7 +10,7 @@
10
10
  "language",
11
11
  "productivity"
12
12
  ],
13
- "repository": "https://github.com/wilhelmeek/lls",
13
+ "repository": "https://github.com/wilhelmeek/linear-ls",
14
14
  "author": "wilhelm <helm@hey.com>",
15
15
  "license": "MIT",
16
16
  "files": [
@@ -18,36 +18,13 @@
18
18
  "index.js"
19
19
  ],
20
20
  "dependencies": {
21
- "@urql/core": "^3.1.1",
22
- "graphql": "^16.6.0",
23
- "node-fetch": "^2.6.7",
24
- "vscode-languageserver": "^8.0.2",
25
- "vscode-languageserver-textdocument": "^1.0.8"
21
+ "@linear/sdk": "^68.1.0",
22
+ "vscode-languageserver": "^9.0.1",
23
+ "vscode-languageserver-textdocument": "^1.0.12"
26
24
  },
27
25
  "devDependencies": {
28
- "@graphql-codegen/cli": "^3.0.0",
29
- "@graphql-codegen/typescript": "^3.0.0",
30
- "@graphql-codegen/typescript-operations": "^3.0.0",
31
- "@types/node-fetch": "^2.6.2",
32
- "@typescript-eslint/eslint-plugin": "^5.50.0",
33
- "@typescript-eslint/parser": "^5.50.0",
34
- "eslint": "^8.33.0",
35
- "prettier": "^2.8.3",
36
- "typescript": "^4.9.5"
37
- },
38
- "eslintConfig": {
39
- "parser": "@typescript-eslint/parser",
40
- "plugins": [
41
- "@typescript-eslint"
42
- ],
43
- "extends": [
44
- "eslint:recommended",
45
- "plugin:@typescript-eslint/recommended"
46
- ],
47
- "rules": {
48
- "@typescript-eslint/no-unused-vars": "error",
49
- "@typescript-eslint/no-explicit-any": "error",
50
- "@typescript-eslint/no-non-null-assertion": "error"
51
- }
26
+ "@biomejs/biome": "^2.3.10",
27
+ "@types/node": "^23",
28
+ "typescript": "^5.9.3"
52
29
  }
53
30
  }
package/dist/linear.js DELETED
@@ -1,115 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.findIssuesByTitle = exports.getIssueByKey = exports.getTeamKeys = void 0;
7
- const core_1 = require("@urql/core");
8
- const node_fetch_1 = __importDefault(require("node-fetch"));
9
- const client = (0, core_1.createClient)({
10
- url: "https://api.linear.app/graphql",
11
- requestPolicy: "cache-first",
12
- exchanges: core_1.defaultExchanges,
13
- fetch: node_fetch_1.default,
14
- fetchOptions: {
15
- headers: {
16
- authorization: process.env.LINEAR_API_KEY,
17
- },
18
- },
19
- });
20
- async function getTeamKeys() {
21
- const resp = await client
22
- .query((0, core_1.gql) `
23
- query FindTeamPrefixes {
24
- teams {
25
- nodes {
26
- id
27
- key
28
- }
29
- }
30
- }
31
- `, {})
32
- .toPromise();
33
- return resp.data?.teams.nodes.map((t) => t.key) ?? [];
34
- }
35
- exports.getTeamKeys = getTeamKeys;
36
- const issueFragment = (0, core_1.gql) `
37
- fragment Issue on Issue {
38
- id
39
- identifier
40
- title
41
- description
42
- url
43
- team {
44
- id
45
- key
46
- }
47
- }
48
- `;
49
- async function getIssueByKey(key) {
50
- const components = key.split("-");
51
- const teamKey = components[0];
52
- if (teamKey?.length !== 3) {
53
- return;
54
- }
55
- const issueNumber = Number(components[1]);
56
- if (!Number.isInteger(issueNumber)) {
57
- return;
58
- }
59
- const resp = await client
60
- .query((0, core_1.gql) `
61
- query GetIssueByKey($filter: IssueFilter!) {
62
- issues(filter: $filter) {
63
- nodes {
64
- ...Issue
65
- }
66
- }
67
- }
68
- ${issueFragment}
69
- `, {
70
- filter: {
71
- team: { key: { eqIgnoreCase: teamKey } },
72
- number: { eq: issueNumber },
73
- },
74
- })
75
- .toPromise();
76
- return resp.data?.issues?.nodes[0];
77
- }
78
- exports.getIssueByKey = getIssueByKey;
79
- async function findIssuesByTitle(teamKeys, issueTitle) {
80
- const resp = await client
81
- .query((0, core_1.gql) `
82
- query FindIssuesByTitle(
83
- $teamFilter: TeamFilter!
84
- $issueFilter: IssueFilter!
85
- ) {
86
- teams(filter: $teamFilter) {
87
- nodes {
88
- id
89
- issues(filter: $issueFilter) {
90
- nodes {
91
- ...Issue
92
- }
93
- }
94
- }
95
- }
96
- }
97
- ${issueFragment}
98
- `, {
99
- teamFilter: {
100
- or: teamKeys.map((tk) => ({
101
- key: {
102
- containsIgnoreCase: tk,
103
- },
104
- })),
105
- },
106
- issueFilter: {
107
- title: {
108
- containsIgnoreCase: issueTitle,
109
- },
110
- },
111
- })
112
- .toPromise();
113
- return resp.data?.teams.nodes.flatMap((team) => team.issues.nodes);
114
- }
115
- exports.findIssuesByTitle = findIssuesByTitle;
@@ -1,2 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });