ace-linters 0.1.0 → 0.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 CHANGED
@@ -1,12 +1,14 @@
1
1
  # Ace Linters (Ace Language Client)
2
2
 
3
- Ace linters is a library that brings language-aware features to the Ace editor. It includes a number of language services, each of which provides support for a specific language, such as JSON, HTML, CSS, and Typescript.
3
+ Ace linters is a library that brings language-aware features to the Ace editor. It includes a number of language
4
+ services, each of which provides support for a specific language, such as JSON, HTML, CSS, and Typescript.
4
5
 
5
6
  Ace linters works in two modes: **WebSockets** and **WebWorkers**.
6
7
 
7
8
  ## Features
8
9
 
9
10
  With Ace linters, you can easily add the following language-aware features to your Ace editor:
11
+
10
12
  - Error checking
11
13
  - Code completion
12
14
  - Type checking
@@ -16,6 +18,7 @@ With Ace linters, you can easily add the following language-aware features to yo
16
18
  ## Supported languages
17
19
 
18
20
  Ace linters supports the following languages by default with webworkers approach:
21
+
19
22
  - JSON, JSON5 (with JsonService)
20
23
  - HTML (with HtmlService)
21
24
  - CSS, SCSS, LESS (with CssService)
@@ -23,6 +26,7 @@ Ace linters supports the following languages by default with webworkers approach
23
26
  - Lua (with LuaService)
24
27
 
25
28
  For WebSockets you could connect any of your Language Server folowing LSP
29
+
26
30
  ## Installation
27
31
 
28
32
  To install Ace linters, you can use the following command:
@@ -31,29 +35,32 @@ To install Ace linters, you can use the following command:
31
35
  npm install ace-linters
32
36
  ```
33
37
 
34
- ## Usage with WebWorker
38
+ ## Usage with WebWorker (JSON-RPC)
35
39
 
36
40
  To use Ace linters with WebWorker, you will first need to include it in your project and create an instance of the Ace
37
- editor
38
- and
39
- an instance of LanguageProvider. Then, LanguageProvider will use the appropriate language service based on the mode being used:
41
+ editor and an instance of LanguageProvider.
40
42
 
41
43
  *client.js*:
44
+
42
45
  ```javascript
43
46
  import * as ace from "ace-code";
44
47
  import {Mode as TypescriptMode} from "ace-code/src/mode/typescript";
45
- import {registerStyles, LanguageProvider, MessageController} from "ace-linters";
46
- import { ScriptTarget, JsxEmit } from "ace-linters/type-converters/typescript-converters";
48
+ import {registerStyles, LanguageProvider} from "ace-linters";
49
+ import {ScriptTarget, JsxEmit} from "ace-linters/type-converters/typescript-converters";
47
50
 
48
- // Register custom styles for the hover tooltip
49
- registerStyles();
50
-
51
- // Create a web worker and a message controller
51
+ // Create a web worker
52
52
  let worker = new Worker(new URL('./webworker.js', import.meta.url));
53
- let messageController = new MessageController(worker);
53
+
54
+ // Create an Ace editor
55
+ let editor = ace.edit("container", {
56
+ mode: new TypescriptMode() // Set the mode of the editor to Typescript
57
+ });
58
+
59
+ // Create a language provider for web worker
60
+ let languageProvider = LanguageProvider.for(worker);
54
61
 
55
62
  // Set global options for the Typescript service
56
- messageController.setGlobalOptions("typescript", {
63
+ languageProvider.setGlobalOptions("typescript", {
57
64
  compilerOptions: {
58
65
  allowJs: true,
59
66
  target: ScriptTarget.ESNext,
@@ -61,25 +68,19 @@ messageController.setGlobalOptions("typescript", {
61
68
  }
62
69
  });
63
70
 
64
- // Create an Ace editor
65
- let editor = ace.edit("container");
66
-
67
- // Set the mode of the editor to Typescript
68
- editor.session.setMode(new TypescriptMode());
69
-
70
- // Create a language provider
71
- let languageProvider = new LanguageProvider(messageController);
72
-
73
71
  // Register the editor with the language provider
74
72
  languageProvider.registerEditor(editor);
75
73
 
76
74
  ```
75
+
77
76
  In WebWorkers mode, you need to register
78
77
  services on the webworker side. Like this:
79
78
 
80
79
  *webworker.js*
80
+
81
81
  ```javascript
82
82
  import {ServiceManager} from "ace-linters/services/service-manager";
83
+
83
84
  let manager = new ServiceManager(self);
84
85
  manager.registerService("typescript", {
85
86
  module: () => import("ace-linters/services/typescript/typescript-service"),
@@ -88,37 +89,31 @@ manager.registerService("typescript", {
88
89
  });
89
90
  ```
90
91
 
91
- [Example client](https://github.com/mkslanc/ace-linters/blob/main/packages/demo/webworker-lsp/demo.ts).
92
+ [Example client](https://github.com/mkslanc/ace-linters/blob/main/packages/demo/webworker-json-rpc/demo.ts)
92
93
 
93
- [Example web worker](https://github.com/mkslanc/ace-linters/blob/main/packages/demo/webworker-lsp/webworker.ts)
94
+ [Example web worker](https://github.com/mkslanc/ace-linters/blob/main/packages/demo/webworker-json-rpc/webworker.ts)
95
+
96
+ ## Usage with WebSocket (JSON-RPC)
94
97
 
95
- ## Usage with WebSocket
96
98
  In WebSockets mode, you need to start a language server on any port and connect to it.
97
99
 
98
100
  Here's an example client:
101
+
99
102
  ```javascript
100
103
  import * as ace from "ace-code";
101
104
  import {Mode as JSONMode} from "ace-code/src/mode/json"; //any mode you want
102
- import {registerStyles, LanguageProvider, MessageControllerWS} from "ace-linters";
103
- import { ScriptTarget, JsxEmit } from "ace-linters/type-converters/typescript-converters";
104
-
105
- // Register custom styles for the hover tooltip
106
- registerStyles();
105
+ import {LanguageProvider} from "ace-linters";
107
106
 
108
- // Create a web socket and a message controller
107
+ // Create a web socket
109
108
  const webSocket = new WebSocket("ws://localhost:3000/exampleServer"); // adress of your websocket server
110
109
 
111
- //Message controller for web sockets
112
- let messageController = new MessageControllerWS(webSocket);
113
-
114
110
  // Create an Ace editor
115
- let editor = ace.edit("container");
116
-
117
- // Set the mode of the editor to JSON
118
- editor.session.setMode(new JSONMode());
111
+ let editor = ace.edit("container", {
112
+ mode: new JSONMode() // Set the mode of the editor to JSON
113
+ });
119
114
 
120
- // Create a language provider
121
- let languageProvider = new LanguageProvider(messageController);
115
+ // Create a language provider for web socket
116
+ let languageProvider = LanguageProvider.for(webSocket);
122
117
 
123
118
  // Register the editor with the language provider
124
119
  languageProvider.registerEditor(editor);
@@ -127,5 +122,11 @@ languageProvider.registerEditor(editor);
127
122
  [Example client](https://github.com/mkslanc/ace-linters/blob/main/packages/demo/websockets-lsp/client.ts)
128
123
 
129
124
  [Example server](https://github.com/mkslanc/ace-linters/tree/main/packages/demo/websockets-lsp/server)
125
+
126
+ ## Usage with WebWorker (other format)
127
+
128
+ You can use Ace linters with pre-defined services by looking at the "Ace linters with WebWorker demo (with default services)" example on GitHub:
129
+ [Example](https://github.com/mkslanc/ace-linters/blob/main/packages/demo/webworker-lsp/)
130
130
  ## License
131
+
131
132
  Ace linters is released under the [MIT License](https://opensource.org/licenses/MIT).
@@ -20,7 +20,7 @@ export class DescriptionTooltip extends Tooltip {
20
20
  this.provider = provider;
21
21
  Tooltip.call(this, document.body);
22
22
 
23
- event.addListener(this.getElement(), "mouseout", this.onTooltipMouseOut);
23
+ event.addListener(this.getElement(), "mouseout", this.onMouseOut);
24
24
 
25
25
  this.getElement().style.pointerEvents = "auto";
26
26
  this.getElement().style.whiteSpace = "pre-wrap";
@@ -30,14 +30,25 @@ export class DescriptionTooltip extends Tooltip {
30
30
  editor.on("mousemove", this.onMouseMove);
31
31
  }
32
32
 
33
- private $registerEditorEvents(editor: Ace.Editor) {
33
+ private $activateEditor(editor: Ace.Editor) {
34
34
  this.$activeEditor = editor;
35
- editor.on("change", this.$hide);
36
- editor.on("mousewheel", this.$hide);
37
- //@ts-ignore
38
- editor.on("mousedown", this.$hide);
39
35
 
40
- editor.container.addEventListener("mouseout", this.onTooltipMouseOut);
36
+ this.$activeEditor.container.addEventListener("mouseout", this.onMouseOut);
37
+ }
38
+
39
+ private $inactivateEditor() {
40
+ if (!this.$activeEditor)
41
+ return;
42
+ this.$activeEditor.container.removeEventListener("mouseout", this.onMouseOut);
43
+
44
+ this.$activeEditor = null;
45
+ }
46
+
47
+ private $registerEditorEvents() {
48
+ this.$activeEditor.on("change", this.$hide);
49
+ this.$activeEditor.on("mousewheel", this.$hide);
50
+ //@ts-ignore
51
+ this.$activeEditor.on("mousedown", this.$hide);
41
52
  }
42
53
 
43
54
  private $removeEditorEvents() {
@@ -46,7 +57,7 @@ export class DescriptionTooltip extends Tooltip {
46
57
  //@ts-ignore
47
58
  this.$activeEditor.off("mousedown", this.$hide);
48
59
 
49
- this.$activeEditor.container.removeEventListener("mouseout", this.onTooltipMouseOut);
60
+ this.$activeEditor.container.removeEventListener("mouseout", this.onMouseOut);
50
61
  this.$activeEditor = null;
51
62
  }
52
63
 
@@ -54,24 +65,23 @@ export class DescriptionTooltip extends Tooltip {
54
65
  clearTimeout(this.$mouseMoveTimer);
55
66
  clearTimeout(this.$showTimer);
56
67
  if (this.isOpen) {
57
- this.doHover(editor);
68
+ this.doHover();
58
69
  } else {
59
70
  this.$mouseMoveTimer = setTimeout(() => {
60
- this.doHover(editor);
71
+ this.$inactivateEditor();
72
+ this.$activateEditor(editor);
73
+ this.doHover();
61
74
  this.$mouseMoveTimer = null;
62
75
  }, 500);
63
76
 
64
77
  }
65
78
  };
66
79
 
67
- doHover = (editor) => {
68
- if (!editor) {
69
- console.log(editor);
70
- }
71
- let renderer = editor.renderer;
80
+ doHover = () => {
81
+ let renderer = this.$activeEditor.renderer;
72
82
  let screenPos = renderer.pixelToScreenCoordinates(this.x, this.y);
73
83
 
74
- let session = editor.session;
84
+ let session = this.$activeEditor.session;
75
85
 
76
86
  this.provider.doHover(session, screenPos, (hover) => {
77
87
  let description = this.provider.getTooltipText(hover);
@@ -99,21 +109,21 @@ export class DescriptionTooltip extends Tooltip {
99
109
  this.column = column;
100
110
 
101
111
  if (this.$mouseMoveTimer) {
102
- this.$show(editor);
112
+ this.$show();
103
113
  } else {
104
114
  this.$showTimer = setTimeout(() => {
105
- this.$show(editor);
115
+ this.$show();
106
116
  this.$showTimer = null;
107
117
  }, 500);
108
118
  }
109
119
  });
110
120
  }
111
121
 
112
- $show = (editor: Editor) => {
113
- let renderer = editor.renderer;
122
+ $show = () => {
123
+ let renderer = this.$activeEditor.renderer;
114
124
  let position = renderer.textToScreenCoordinates(this.row, this.column);
115
125
 
116
- let cursorPos = editor.getCursorPosition();
126
+ let cursorPos = this.$activeEditor.getCursorPosition();
117
127
 
118
128
  this.show(null, position.pageX, position.pageY);
119
129
 
@@ -138,7 +148,7 @@ export class DescriptionTooltip extends Tooltip {
138
148
  else
139
149
  position.pageY += renderer.lineHeight;
140
150
 
141
- this.$registerEditorEvents(editor);
151
+ this.$registerEditorEvents();
142
152
 
143
153
  this.getElement().style.maxWidth = rect.width - (position.pageX - rect.left) + "px";
144
154
  this.show(null, position.pageX, position.pageY);
@@ -151,12 +161,8 @@ export class DescriptionTooltip extends Tooltip {
151
161
  };
152
162
 
153
163
  onMouseOut = (e: MouseEvent) => {
154
- if (e && e.relatedTarget == this.getElement())
155
- return;
156
- this.$hide();
157
- };
158
-
159
- onTooltipMouseOut = (e: MouseEvent) => {
164
+ clearTimeout(this.$mouseMoveTimer);
165
+ clearTimeout(this.$showTimer);
160
166
  if (!e.relatedTarget || e.relatedTarget == this.getElement())
161
167
  return;
162
168
 
@@ -169,14 +175,15 @@ export class DescriptionTooltip extends Tooltip {
169
175
  }
170
176
 
171
177
  $hide = () => {
172
- clearTimeout(this.$timer);
178
+ clearTimeout(this.$mouseMoveTimer);
179
+ clearTimeout(this.$showTimer);
173
180
  this.$removeEditorEvents();
174
181
  this.hide();
175
182
  }
176
183
 
177
184
  destroy() {
178
185
  this.$hide();
179
- event.removeListener(this.getElement(), "mouseout", this.onTooltipMouseOut);
186
+ event.removeListener(this.getElement(), "mouseout", this.onMouseOut);
180
187
  };
181
188
 
182
189
  }
package/index.ts CHANGED
@@ -2,9 +2,7 @@ import {MessageController} from "./message-controller";
2
2
  import * as lintersCSS from "./css/linters.css";
3
3
  import * as dom from "ace-code/src/lib/dom";
4
4
 
5
- export function registerStyles() {
6
- dom.importCssString(lintersCSS, "linters.css");
7
- }
5
+ dom.importCssString(lintersCSS, "linters.css");
8
6
 
9
7
  export {LanguageProvider} from "./language-provider";
10
8
  export {MessageController} from "./message-controller";
@@ -2,7 +2,6 @@ import {Ace, Range as AceRange} from "ace-code";
2
2
  import {DescriptionTooltip} from "./components/description-tooltip";
3
3
  import {AceLinters} from "./types";
4
4
  import Tooltip = AceLinters.Tooltip;
5
- import TextEdit = AceLinters.TextEdit;
6
5
  import {FormattingOptions} from "vscode-languageserver-protocol";
7
6
  import {MarkDownConverter} from "./types";
8
7
  import {CommonConverter} from "./type-converters/common-converters";
@@ -11,7 +10,20 @@ import ServiceOptions = AceLinters.ServiceOptions;
11
10
  import Editor = Ace.Editor;
12
11
  import EditSession = Ace.EditSession;
13
12
  import Completion = Ace.Completion;
14
- import Annotation = Ace.Annotation;
13
+ import {MessageControllerWS} from "./message-controller-ws";
14
+ import ServiceOptionsMap = AceLinters.ServiceOptionsMap;
15
+ import {MessageController} from "./message-controller";
16
+ import {
17
+ fromAceDelta,
18
+ fromPoint,
19
+ fromRange,
20
+ toAnnotations,
21
+ toCompletionItem,
22
+ toCompletions,
23
+ toRange, toResolvedCompletion,
24
+ toTooltip
25
+ } from "./type-converters/lsp-converters";
26
+ import * as lsp from "vscode-languageserver-protocol";
15
27
 
16
28
  let showdown = require('showdown');
17
29
 
@@ -20,15 +32,37 @@ export class LanguageProvider {
20
32
  private $descriptionTooltip: DescriptionTooltip;
21
33
  private readonly $markdownConverter: MarkDownConverter;
22
34
  private readonly $messageController: IMessageController;
23
- private $sessionLanguageProviders: {[sessionID: string]: SessionLanguageProvider} = {};
35
+ private $sessionLanguageProviders: { [sessionID: string]: SessionLanguageProvider } = {};
24
36
  private $editors: Editor[] = [];
25
37
 
26
- constructor(messageController: IMessageController, markdownConverter?: MarkDownConverter) {
38
+ private constructor(messageController: IMessageController, markdownConverter?: MarkDownConverter) {
27
39
  this.$messageController = messageController;
28
40
  this.$markdownConverter = markdownConverter ?? new showdown.Converter();
29
41
  this.$descriptionTooltip = new DescriptionTooltip(this);
30
42
  }
31
43
 
44
+ /**
45
+ * Creates LanguageProvider for any Language Server to connect with JSON-RPC (webworker, websocket)
46
+ * @param {Worker | WebSocket} mode
47
+ * @param markdownConverter
48
+ */
49
+ static for(mode: Worker | WebSocket, markdownConverter?: MarkDownConverter) { //TODO:
50
+ let messageController = new MessageControllerWS(mode);
51
+ return new LanguageProvider(messageController, markdownConverter);
52
+ }
53
+
54
+ /**
55
+ * Creates LanguageProvider using our transport protocol with ability to register different services on same
56
+ * webworker
57
+ * @param {Worker} worker
58
+ * @param markdownConverter
59
+ */
60
+ static default(worker: Worker, markdownConverter?: MarkDownConverter) {
61
+ let messageController: IMessageController;
62
+ messageController = new MessageController(worker);
63
+ return new LanguageProvider(messageController, markdownConverter);
64
+ }
65
+
32
66
  private $registerSession = (session?: EditSession, options?: ServiceOptions) => {
33
67
  if (!session)
34
68
  return;
@@ -66,14 +100,19 @@ export class LanguageProvider {
66
100
  sessionLanguageProvider.setOptions(options);
67
101
  }
68
102
 
103
+ setGlobalOptions<T extends keyof ServiceOptionsMap>(serviceName: T, options: ServiceOptionsMap[T], merge = false) {
104
+ this.$messageController.setGlobalOptions(serviceName, options, merge);
105
+ }
106
+
69
107
  doHover(session: EditSession, position: Ace.Point, callback?: (hover: Tooltip) => void) {
70
- this.$messageController.doHover(this.$getFileName(session), position, callback);
108
+ this.$messageController.doHover(this.$getFileName(session), fromPoint(position), (hover) => callback(toTooltip(hover)));
71
109
  }
72
110
 
73
111
  getTooltipText(hover: Tooltip) {
74
112
  if (!hover)
75
113
  return;
76
- let text = hover.content.type === CommonConverter.TooltipType.markdown ? CommonConverter.cleanHtml(this.$markdownConverter.makeHtml(hover.content.text)) : hover.content.text;
114
+ let text = hover.content.type === CommonConverter.TooltipType.markdown ?
115
+ CommonConverter.cleanHtml(this.$markdownConverter.makeHtml(hover.content.text)) : hover.content.text;
77
116
  return {text: text, range: hover.range}
78
117
  }
79
118
 
@@ -82,9 +121,10 @@ export class LanguageProvider {
82
121
  sessionLanguageProvider.format();
83
122
  }
84
123
 
85
- doComplete(editor: Editor, session: EditSession, callback: (CompletionList: Completion[]) => void) {
124
+ doComplete(editor: Editor, session: EditSession, callback: (CompletionList: Completion[] | null) => void) {
86
125
  let cursor = editor.getCursorPosition();
87
- this.$messageController.doComplete(this.$getFileName(session), cursor, callback);
126
+ this.$messageController.doComplete(this.$getFileName(session), fromPoint(cursor),
127
+ (completionList) => callback(toCompletions(completionList)));
88
128
  }
89
129
 
90
130
  $registerCompleters(editor: Editor) {
@@ -93,14 +133,17 @@ export class LanguageProvider {
93
133
  getCompletions: async (editor, session, pos, prefix, callback) => {
94
134
  this.doComplete(editor, session, (completions) => {
95
135
  let fileName = this.$getFileName(session);
136
+ if (!completions)
137
+ return;
96
138
  completions.forEach((item) => item["fileName"] = fileName);
97
139
  callback(null, CommonConverter.normalizeRanges(completions));
98
140
  });
99
141
  },
100
142
  getDocTooltip: (item) => {
101
143
  if (!item["isResolved"]) {
102
- this.$messageController.doResolve(item["fileName"], item, (completion) => {
144
+ this.$messageController.doResolve(item["fileName"], toCompletionItem(item), (completionItem) => {
103
145
  item["isResolved"] = true;
146
+ let completion = toResolvedCompletion(item, completionItem);
104
147
  item.docText = completion.docText;
105
148
  if (completion.docHTML) {
106
149
  item.docHTML = completion.docHTML;
@@ -139,12 +182,14 @@ class SessionLanguageProvider {
139
182
  this.$messageController = messageController;
140
183
  this.session = session;
141
184
  this.initFileName();
185
+
186
+ session.doc["version"] = 0;
142
187
  session.doc.on("change", this.$changeListener, true);
143
188
 
144
189
  // @ts-ignore
145
190
  session.on("changeMode", this.$changeMode);
146
191
 
147
- this.$messageController.init(this.fileName, session.getValue(), this.$mode, options, this.$connected, this.$showAnnotations);
192
+ this.$messageController.init(this.fileName, session.doc, this.$mode, options, this.$connected, this.$showAnnotations);
148
193
  }
149
194
 
150
195
  private $connected = () => {
@@ -187,13 +232,12 @@ class SessionLanguageProvider {
187
232
  }
188
233
 
189
234
  private $changeListener = (delta) => {
235
+ this.session.doc["version"]++;
190
236
  if (!this.$deltaQueue) {
191
237
  this.$deltaQueue = [];
192
238
  setTimeout(this.$sendDeltaQueue, 0);
193
239
  }
194
- if (delta.action == "insert")
195
- this.$deltaQueue.push(delta.start, delta.lines);
196
- else this.$deltaQueue.push(delta.start, delta.end);
240
+ this.$deltaQueue.push(delta);
197
241
  }
198
242
 
199
243
  private $sendDeltaQueue = () => {
@@ -201,11 +245,13 @@ class SessionLanguageProvider {
201
245
  if (!deltas) return;
202
246
  this.$deltaQueue = null;
203
247
  if (deltas.length)
204
- this.$messageController.change(this.fileName, deltas, this.session.getValue(), this.session.doc.getLength());
248
+ this.$messageController.change(this.fileName, deltas.map((delta) =>
249
+ fromAceDelta(delta, this.session.doc.getNewLineCharacter())), this.session.doc);
205
250
  };
206
251
 
207
- private $showAnnotations = (annotations: Annotation[]) => {
252
+ private $showAnnotations = (diagnostics) => {
208
253
  this.session.clearAnnotations();
254
+ let annotations = toAnnotations(diagnostics)
209
255
  if (annotations && annotations.length > 0) {
210
256
  this.session.setAnnotations(annotations);
211
257
  }
@@ -232,19 +278,19 @@ class SessionLanguageProvider {
232
278
  selectionRanges = [new AceRange(0, 0, row, column)];
233
279
  }
234
280
  for (let range of selectionRanges) {
235
- this.$messageController.format(this.fileName, range, $format, this.$applyFormat);
281
+ this.$messageController.format(this.fileName, fromRange(range), $format, this.$applyFormat);
236
282
  }
237
283
  }
238
284
 
239
- private $applyFormat = (edits: TextEdit[]) => {
240
- for (let edit of edits) {
241
- this.session.doc.replace(CommonConverter.toRange(edit.range), edit.newText); //we need this to
242
- // mirror Range
285
+ private $applyFormat = (edits: lsp.TextEdit[]) => {
286
+ for (let edit of edits.reverse()) {
287
+ this.session.doc.replace(toRange(edit.range), edit.newText);
243
288
  }
244
289
  }
245
290
 
246
291
  doComplete(editor: Editor, callback: (CompletionList) => void) {
247
292
  let cursor = editor.getCursorPosition();
248
- this.$messageController.doComplete(this.fileName, cursor, callback);
293
+ this.$messageController.doComplete(this.fileName, fromPoint(cursor),
294
+ (completionList) => callback(toCompletions(completionList)));
249
295
  }
250
- }
296
+ }