yoda-language-server 0.8.0 → 0.9.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.
@@ -30,6 +30,12 @@
30
30
  ],
31
31
  "default": null,
32
32
  "description": "Specifies the path of yoda."
33
+ },
34
+ "yoda.trace.server": {
35
+ "type": "string",
36
+ "enum": ["off", "messages", "compact", "verbose"],
37
+ "default": "off",
38
+ "description": "Message level of yoda server."
33
39
  }
34
40
  }
35
41
  }
@@ -42,16 +48,20 @@
42
48
  "package": "vsce package"
43
49
  },
44
50
  "dependencies": {
51
+ "semver": "^7.3.5",
45
52
  "vscode-languageclient": "^7.0"
46
53
  },
47
54
  "devDependencies": {
48
- "@types/mocha": "^5.2.6",
49
- "@types/node": "^8.10.66",
50
- "@types/vscode": "^1.52.0",
51
- "glob": "^7.1.4",
52
- "mocha": "^9.1.3",
55
+ "@types/chai": "^4.3",
56
+ "@types/mocha": "^9.1",
57
+ "@types/node": "^8.10",
58
+ "@types/semver": "^7.3.9",
59
+ "@types/vscode": "^1.52",
60
+ "@vscode/test-electron": "^2.1",
61
+ "chai": "^4.3",
62
+ "glob": "^7.1",
63
+ "mocha": "^9.1",
53
64
  "typescript": "^4",
54
- "vsce": "^2.6.0",
55
- "vscode-test": "^1.5"
65
+ "vsce": "^2.6.0"
56
66
  }
57
67
  }
@@ -0,0 +1,55 @@
1
+
2
+ import { execSync } from 'child_process'
3
+ import { cmp, maxSatisfying } from 'semver'
4
+ import { asyncExec } from './utils'
5
+
6
+ interface CheckResult {
7
+ shouldUpdate: boolean
8
+ localVersion: string
9
+ remoteVersion: string
10
+ }
11
+
12
+ export async function checkVersions(): Promise<CheckResult> {
13
+ const { stdout } = await asyncExec("gem list --both --exact yoda-language-server")
14
+ const [localVersion, remoteVersion] = parseGemList(stdout)
15
+
16
+ return {
17
+ shouldUpdate: shouldUpdate(localVersion, remoteVersion),
18
+ localVersion: localVersion,
19
+ remoteVersion: remoteVersion,
20
+ }
21
+ }
22
+
23
+ function shouldUpdate(localVersion: string, remoteVersion: string): boolean {
24
+ if (!localVersion) {
25
+ return true
26
+ }
27
+
28
+ if (!remoteVersion) {
29
+ return false
30
+ }
31
+
32
+ return cmp(localVersion, "<", remoteVersion)
33
+ }
34
+
35
+ function parseGemList(stdout: string): [string, string] {
36
+ const [local, remote] = stdout.split("*** REMOTE GEMS ***")
37
+
38
+ const localVersion = extractVersion(local)
39
+ const remoteVersion = extractVersion(remote)
40
+
41
+ return [localVersion, remoteVersion]
42
+ }
43
+
44
+ function extractVersion(text: string): string {
45
+ const lines = text.split("\n")
46
+ for (const line of lines) {
47
+ const matchData = line.match(/^yoda-language-server\s*\((.+)\)/);
48
+ if (matchData) {
49
+ const versions = matchData[1].split(/,\s*/)
50
+ return maxSatisfying(versions, '*')
51
+ }
52
+ }
53
+
54
+ return null
55
+ }
@@ -8,6 +8,13 @@ export function calcExecutionConfiguration() {
8
8
  return { command }
9
9
  }
10
10
 
11
- export function getTraceConfiguration (): string | null {
11
+ export function isCustomExecutionPathConfigured(): boolean {
12
+ const yodaPathEnv = process.env.YODA_EXECUTABLE_PATH
13
+ const yodaPathConfiguration = workspace.getConfiguration("yoda").get("path") as (string | null);
14
+
15
+ return !!(yodaPathEnv || yodaPathConfiguration)
16
+ }
17
+
18
+ export function getTraceConfiguration(): string | null {
12
19
  return workspace.getConfiguration("yoda").get("trace.server") as (string | null);
13
20
  }
@@ -1,5 +1,6 @@
1
1
  import { ExtensionContext, Disposable } from 'vscode'
2
- import { isLanguageServerInstalled, promptForInstallTool } from './install-tools'
2
+ import { isCustomExecutionPathConfigured } from './config'
3
+ import { tryInstallOrUpdate } from './install-tools'
3
4
  import { configureLanguageServer } from './language-server'
4
5
 
5
6
  let disposable: Disposable
@@ -11,8 +12,8 @@ export async function activate(context: ExtensionContext) {
11
12
  // This line of code will only be executed once when your extension is activated
12
13
  // console.log('Congratulations, your extension "yoda" is now active!');
13
14
 
14
- if (!isLanguageServerInstalled()) {
15
- await promptForInstallTool()
15
+ if (!isCustomExecutionPathConfigured()) {
16
+ await tryInstallOrUpdate()
16
17
  }
17
18
 
18
19
  const languageServer = configureLanguageServer()
@@ -1,17 +1,10 @@
1
1
  import * as child_process from 'child_process'
2
2
 
3
3
  import { window } from 'vscode'
4
+ import { checkVersions } from './check-versions'
4
5
  import { calcExecutionConfiguration, getTraceConfiguration } from './config'
5
6
  import { outputChannel } from './status'
6
- import { promisify } from 'util'
7
-
8
- function execCommand(command: string, onMessage: (stdout: string | null, stderr: string | null) => void, callback: (error: Error) => void) {
9
- const process = child_process.exec(command, callback)
10
- process.stdout.on('data', (data) => onMessage(data.toString(), null))
11
- process.stderr.on('data', (data) => onMessage(null, data.toString()))
12
- }
13
-
14
- const asyncExecCommand = promisify(execCommand)
7
+ import { asyncExec, asyncExecPipeline } from './utils'
15
8
 
16
9
  export function isLanguageServerInstalled(): boolean {
17
10
  const { command } = calcExecutionConfiguration()
@@ -23,11 +16,41 @@ export function isLanguageServerInstalled(): boolean {
23
16
  }
24
17
  }
25
18
 
26
- export async function promptForInstallTool() {
27
- const choises = ['Install']
28
- const selected = await window.showInformationMessage('yoda command is not available. Please install.', ...choises)
19
+ export async function tryInstallOrUpdate() {
20
+ try {
21
+ if (!isLanguageServerInstalled()) {
22
+ outputChannel.appendLine(`Yoda is not installed. Ask to install.`)
23
+ await promptForInstallTool(false)
24
+ return
25
+ }
26
+
27
+ const { shouldUpdate, localVersion, remoteVersion } = await checkVersions()
28
+
29
+ console.log(`Local version: ${localVersion}`)
30
+ console.log(`Available version: ${remoteVersion}`)
31
+ console.log(`shouldUpdate: ${shouldUpdate}`)
32
+
33
+ if (shouldUpdate) {
34
+ await promptForInstallTool(localVersion !== null, remoteVersion)
35
+ }
36
+ } catch (e) {
37
+ outputChannel.appendLine(`An error occured on update: ${e}`)
38
+ }
39
+ }
40
+
41
+ export async function promptForInstallTool(update: boolean, newVersion?: string) {
42
+ const choises = [update ? 'Update' : 'Install']
43
+
44
+ const newVersionLabel = newVersion ? ` (${newVersion})` : ''
45
+
46
+ const message = update ?
47
+ `A newer version of yoda${newVersionLabel} is updatable.` :
48
+ 'yoda command is not available. Please install.'
49
+
50
+ const selected = await window.showInformationMessage(message, ...choises)
29
51
  switch (selected) {
30
52
  case 'Install':
53
+ case 'Update':
31
54
  await installTool()
32
55
  break;
33
56
  default:
@@ -41,11 +64,29 @@ async function installTool() {
41
64
 
42
65
  outputChannel.appendLine('Installing yoda...')
43
66
 
67
+ await installGemFromRubygems()
68
+
69
+ outputChannel.appendLine('yoda is installed.')
70
+ }
71
+
72
+ async function installGemFromRubygems() {
73
+ outputChannel.appendLine('gem install yoda-language-server')
74
+ await asyncExecPipeline("yes | gem install yoda-language-server", (stdout, stderr) => {
75
+ if (stdout) {
76
+ outputChannel.append(stdout)
77
+ }
78
+ if (stderr) {
79
+ outputChannel.append(stderr)
80
+ }
81
+ })
82
+ }
83
+
84
+ async function installGemFromRepository() {
44
85
  try {
45
- child_process.execSync("gem list --installed --exact specific_install")
86
+ await asyncExec("gem list --installed --exact specific_install")
46
87
  } catch (e) {
47
88
  outputChannel.appendLine('gem install specific_install')
48
- await asyncExecCommand("gem install specific_install", (stdout, stderr) => {
89
+ await asyncExecPipeline("gem install specific_install", (stdout, stderr) => {
49
90
  if (stdout) {
50
91
  outputChannel.append(stdout)
51
92
  }
@@ -57,7 +98,7 @@ async function installTool() {
57
98
  }
58
99
 
59
100
  outputChannel.appendLine('gem specific_install tomoasleep/yoda')
60
- await asyncExecCommand("gem specific_install tomoasleep/yoda", (stdout, stderr) => {
101
+ await asyncExecPipeline("gem specific_install tomoasleep/yoda", (stdout, stderr) => {
61
102
  if (stdout) {
62
103
  outputChannel.append(stdout)
63
104
  }
@@ -65,5 +106,4 @@ async function installTool() {
65
106
  outputChannel.append(stderr)
66
107
  }
67
108
  })
68
- outputChannel.appendLine('yoda is installed.')
69
109
  }
@@ -19,6 +19,7 @@ export function configureLanguageServer(): LanguageClient {
19
19
  }
20
20
 
21
21
  const clientOptions : LanguageClientOptions = {
22
+ progressOnInitialization: true,
22
23
  documentSelector: [{ scheme: 'file', language: 'ruby' }],
23
24
  synchronize: {
24
25
  configurationSection: 'yoda',
@@ -37,6 +38,10 @@ export function configureLanguageServer(): LanguageClient {
37
38
  outputChannel.appendLine(value);
38
39
  console.log(value);
39
40
  },
41
+ replace(value: string) {
42
+ outputChannel.replace(value);
43
+ console.log(value);
44
+ },
40
45
  clear() { outputChannel.clear() },
41
46
  show() { outputChannel.show() },
42
47
  hide() { outputChannel.hide() },
@@ -1,6 +1,6 @@
1
1
  import * as path from 'path';
2
2
 
3
- import { runTests } from 'vscode-test';
3
+ import { runTests } from '@vscode/test-electron';
4
4
 
5
5
  async function main() {
6
6
  try {
@@ -1,43 +1,31 @@
1
1
  import * as vscode from 'vscode';
2
- import * as assert from 'assert';
2
+ import { expect } from 'chai';
3
3
  import { getDocUri, activate } from '../helper';
4
4
 
5
5
  describe('Should provide hover', () => {
6
6
  const docUri = getDocUri('completion.rb');
7
7
 
8
8
  it('show hover', async () => {
9
- await testCompletion(docUri, new vscode.Position(0, 2), {
10
- contents: [
11
- {"language":"ruby","value":"Object # Object.module"},
12
- "**Object.class**\n\n\n",
13
- ],
14
- range: new vscode.Range(
15
- new vscode.Position(0, 0),
16
- new vscode.Position(0, 6),
17
- ),
18
- });
19
- })
9
+ await activate(docUri);
10
+
11
+ const actualHovers = await requestComplete(docUri, new vscode.Position(0, 2));
20
12
 
13
+ console.log("hovers: ", actualHovers);
21
14
 
15
+ expect((actualHovers[0].contents[0] as vscode.MarkdownString).value).to.include("Object # singleton(::Object)");
16
+ expect((actualHovers[0].contents[1] as vscode.MarkdownString).value).to.include("**Object**")
17
+ })
22
18
  });
23
19
 
24
- async function testCompletion(
20
+ async function requestComplete(
25
21
  docUri: vscode.Uri,
26
22
  position: vscode.Position,
27
- expectedHover: vscode.Hover
28
- ) {
29
- await activate(docUri);
30
-
31
- // Executing the command `vscode.executeCompletionItemProvider` to simulate triggering completion
32
- // See: https://code.visualstudio.com/api/references/commands
33
- const [actualHover] = await vscode.commands.executeCommand<vscode.Hover[]>(
23
+ ): Promise<vscode.Hover[]> {
24
+ const hovers = await vscode.commands.executeCommand<vscode.Hover[]>(
34
25
  'vscode.executeHoverProvider',
35
26
  docUri,
36
- position
27
+ position,
37
28
  );
38
29
 
39
- // assert.equal(actualHover.range, expectedHover.range);
40
- expectedHover.contents.forEach((expectedItem, i) => {
41
- assert.equal(actualHover.contents[i], expectedItem);
42
- });
30
+ return hovers
43
31
  }
@@ -7,10 +7,9 @@ export function run(testsRoot: string, callback: (error: any, failures?: number)
7
7
  const mocha = new Mocha({
8
8
  ui: 'bdd',
9
9
  timeout: 600000,
10
+ color: true,
10
11
  });
11
12
 
12
- mocha.useColors(true);
13
-
14
13
  glob('**/**.test.js', { cwd: testsRoot }, (error, files) => {
15
14
  if (error) {
16
15
  return callback(error);
@@ -0,0 +1,11 @@
1
+ import { exec } from 'child_process'
2
+ import { promisify } from 'util'
3
+
4
+ function execPipeline(command: string, onMessage: (stdout: string | null, stderr: string | null) => void, callback: (error: Error) => void) {
5
+ const process = exec(command, callback)
6
+ process.stdout.on('data', (data) => onMessage(data.toString(), null))
7
+ process.stderr.on('data', (data) => onMessage(null, data.toString()))
8
+ }
9
+
10
+ export const asyncExecPipeline = promisify(execPipeline)
11
+ export const asyncExec = promisify(exec)
@@ -130,5 +130,12 @@ module Yoda
130
130
  def registry_dump(index: nil, length: nil)
131
131
  emit(:registry_dump, index: index, length: length)
132
132
  end
133
+
134
+ # @param name [String]
135
+ # @param version [String]
136
+ # @param message [String]
137
+ def build_library_registry(message:, name:, version:)
138
+ emit(:build_library_registry, name: name, version: version, message: message)
139
+ end
133
140
  end
134
141
  end
@@ -2,7 +2,7 @@ module Yoda
2
2
  class Server
3
3
  # Wrapper class for writer to make thread safe
4
4
  class ConcurrentWriter
5
- # @param [::LanguageServer::Protocol::Transport::Stdio::Writer]
5
+ # @param channel [::LanguageServer::Protocol::Transport::Stdio::Writer]
6
6
  def initialize(channel)
7
7
  @channel = channel
8
8
  @mutex = Mutex.new
@@ -1,9 +1,12 @@
1
1
  require 'concurrent'
2
+ require 'yoda/server/providers'
2
3
 
3
4
  module Yoda
4
5
  class Server
5
6
  # Handle
6
7
  class LifecycleHandler
8
+ include Providers::ReportableProgress
9
+
7
10
  # @return [Session, nil]
8
11
  attr_reader :session
9
12
 
@@ -41,83 +44,92 @@ module Yoda
41
44
 
42
45
  # @param params [LanguageServer::Protocol::Interface::InitializeParams]
43
46
  def handle_initialize(params)
44
- Instrument.instance.hear(initialization_progress: method(:notify_initialization_progress)) do
45
- @session = begin
46
- if params[:workspace_folders]
47
- workspace_folders = params[:workspace_folders].map { |hash| LanguageServer::Protocol::Interface::WorkspaceFolder.new(name: hash[:name], uri: hash[:uri]) }
48
- Session.from_workspace_folders(workspace_folders)
49
- elsif params[:root_uri]
50
- Session.from_root_uri(params[:root_uri])
51
- else
52
- Session.new(workspaces: [])
47
+ in_progress(params, title: "Initializing Yoda") do |progress_reporter|
48
+ reporter = InitializationProgressReporter.new(progress_reporter)
49
+
50
+ subscriptions = {
51
+ initialization_progress: reporter.public_method(:notify_initialization_progress),
52
+ build_library_registry: reporter.public_method(:notify_build_library_registry),
53
+ }
54
+
55
+ Instrument.instance.hear(**subscriptions) do
56
+ @session = begin
57
+ if params[:workspace_folders]
58
+ workspace_folders = params[:workspace_folders].map { |hash| LanguageServer::Protocol::Interface::WorkspaceFolder.new(name: hash[:name], uri: hash[:uri]) }
59
+ Session.from_workspace_folders(workspace_folders)
60
+ elsif params[:root_uri]
61
+ Session.from_root_uri(params[:root_uri])
62
+ else
63
+ Session.new(workspaces: [])
64
+ end
53
65
  end
54
- end
55
66
 
56
- send_warnings(@session.setup || [])
67
+ send_warnings(@session.setup || [])
68
+ end
69
+ end
57
70
 
58
- LanguageServer::Protocol::Interface::InitializeResult.new(
59
- server_info: {
60
- name: "yoda",
61
- version: Yoda::VERSION,
62
- },
63
- capabilities: LanguageServer::Protocol::Interface::ServerCapabilities.new(
64
- text_document_sync: LanguageServer::Protocol::Interface::TextDocumentSyncOptions.new(
65
- open_close: true,
66
- change: LanguageServer::Protocol::Constant::TextDocumentSyncKind::FULL,
67
- save: LanguageServer::Protocol::Interface::SaveOptions.new(
68
- include_text: true,
69
- ),
70
- ),
71
- completion_provider: LanguageServer::Protocol::Interface::CompletionOptions.new(
72
- resolve_provider: false,
73
- trigger_characters: ['.', '@', '[', ':', '!', '<'],
71
+ LanguageServer::Protocol::Interface::InitializeResult.new(
72
+ server_info: {
73
+ name: "yoda",
74
+ version: Yoda::VERSION,
75
+ },
76
+ capabilities: LanguageServer::Protocol::Interface::ServerCapabilities.new(
77
+ text_document_sync: LanguageServer::Protocol::Interface::TextDocumentSyncOptions.new(
78
+ open_close: true,
79
+ change: LanguageServer::Protocol::Constant::TextDocumentSyncKind::FULL,
80
+ save: LanguageServer::Protocol::Interface::SaveOptions.new(
81
+ include_text: true,
74
82
  ),
75
- hover_provider: true,
76
- definition_provider: true,
77
- signature_help_provider: LanguageServer::Protocol::Interface::SignatureHelpOptions.new(
78
- trigger_characters: ['(', ','],
79
- ),
80
- workspace_symbol_provider: LanguageServer::Protocol::Interface::WorkspaceSymbolOptions.new(
81
- work_done_progress: true,
83
+ ),
84
+ completion_provider: LanguageServer::Protocol::Interface::CompletionOptions.new(
85
+ resolve_provider: false,
86
+ trigger_characters: ['.', '@', '[', ':', '!', '<'],
87
+ ),
88
+ hover_provider: true,
89
+ definition_provider: true,
90
+ signature_help_provider: LanguageServer::Protocol::Interface::SignatureHelpOptions.new(
91
+ trigger_characters: ['(', ','],
92
+ ),
93
+ workspace_symbol_provider: LanguageServer::Protocol::Interface::WorkspaceSymbolOptions.new(
94
+ work_done_progress: true,
95
+ ),
96
+ workspace: {
97
+ workspaceFolders: LanguageServer::Protocol::Interface::WorkspaceFoldersServerCapabilities.new(
98
+ supported: true,
99
+ change_notifications: true,
82
100
  ),
83
- workspace: {
84
- workspaceFolders: LanguageServer::Protocol::Interface::WorkspaceFoldersServerCapabilities.new(
85
- supported: true,
86
- change_notifications: true,
87
- ),
88
- fileOperations: {
89
- didCreate: LanguageServer::Protocol::Interface::FileOperationRegistrationOptions.new(
90
- filters: [
91
- LanguageServer::Protocol::Interface::FileOperationFilter.new(
92
- pattern: LanguageServer::Protocol::Interface::FileOperationPattern.new(
93
- glob: "**/*",
94
- ),
101
+ fileOperations: {
102
+ didCreate: LanguageServer::Protocol::Interface::FileOperationRegistrationOptions.new(
103
+ filters: [
104
+ LanguageServer::Protocol::Interface::FileOperationFilter.new(
105
+ pattern: LanguageServer::Protocol::Interface::FileOperationPattern.new(
106
+ glob: "**/*",
95
107
  ),
96
- ],
97
- ),
98
- didRename: LanguageServer::Protocol::Interface::FileOperationRegistrationOptions.new(
99
- filters: [
100
- LanguageServer::Protocol::Interface::FileOperationFilter.new(
101
- pattern: LanguageServer::Protocol::Interface::FileOperationPattern.new(
102
- glob: "**/*",
103
- ),
108
+ ),
109
+ ],
110
+ ),
111
+ didRename: LanguageServer::Protocol::Interface::FileOperationRegistrationOptions.new(
112
+ filters: [
113
+ LanguageServer::Protocol::Interface::FileOperationFilter.new(
114
+ pattern: LanguageServer::Protocol::Interface::FileOperationPattern.new(
115
+ glob: "**/*",
104
116
  ),
105
- ],
106
- ),
107
- didDelete: LanguageServer::Protocol::Interface::FileOperationRegistrationOptions.new(
108
- filters: [
109
- LanguageServer::Protocol::Interface::FileOperationFilter.new(
110
- pattern: LanguageServer::Protocol::Interface::FileOperationPattern.new(
111
- glob: "**/*",
112
- ),
117
+ ),
118
+ ],
119
+ ),
120
+ didDelete: LanguageServer::Protocol::Interface::FileOperationRegistrationOptions.new(
121
+ filters: [
122
+ LanguageServer::Protocol::Interface::FileOperationFilter.new(
123
+ pattern: LanguageServer::Protocol::Interface::FileOperationPattern.new(
124
+ glob: "**/*",
113
125
  ),
114
- ],
115
- ),
116
- },
126
+ ),
127
+ ],
128
+ ),
117
129
  },
118
- ),
119
- )
120
- end
130
+ },
131
+ ),
132
+ )
121
133
  rescue => e
122
134
  Logger.warn e.full_message
123
135
  LanguageServer::Protocol::Interface::ResponseError.new(
@@ -206,18 +218,29 @@ module Yoda
206
218
  EOS
207
219
  end
208
220
 
209
- def notify_initialization_progress(phase: nil, message: nil, index:, length:)
210
- if length && length > 0
211
- percentage = (index || 0) * 100 / length
212
- if index <= 0
213
- notifier.start_progress(id: phase, title: phase, message: message, percentage: percentage)
214
- elsif index >= length
215
- notifier.done_progress(id: phase)
221
+ class InitializationProgressReporter
222
+ # @return [Providers::ReportableProgress::ProgressReporter]
223
+ attr_reader :progress_reporter
224
+
225
+ # @param progress_reporter [Providers::ReportableProgress::ProgressReporter]
226
+ def initialize(progress_reporter)
227
+ @progress_reporter = progress_reporter
228
+ end
229
+
230
+ def notify_initialization_progress(phase: nil, message: nil, index:, length:)
231
+ if length && length > 0
232
+ percentage = (index || 0) * 100 / length
233
+
234
+ progress_reporter.report(message: message, percentage: percentage)
216
235
  else
217
- notifier.report_progress(id: phase, message: message, percentage: percentage)
236
+ progress_reporter.report(message: message)
218
237
  end
219
- else
220
- notifier.event(type: :initialization, phase: phase, message: message)
238
+
239
+ progress_reporter.notifier.event(type: :initialization, phase: phase, message: message)
240
+ end
241
+
242
+ def notify_build_library_registry(message: nil, name: nil, version: nil)
243
+ progress_reporter.report(message: message)
221
244
  end
222
245
  end
223
246
  end