yoda-language-server 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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