Skip to content
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"node": true,
"es6": true
},
"root": true,
"extends": [
"standard-with-typescript",
"plugin:@typescript-eslint/recommended"
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ MATLAB® language server implements the Microsoft® [Language Server Proto

## Features Implemented
MATLAB language server implements several Language Server Protocol features and their related services:
* Code diagnostics — [publishDiagnostics](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_publishDiagnostics)
* Quick fixes — [codeActionProvider](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_codeAction)
* Document formatting — [documentFormattingProvider](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting)
* Code diagnostics — [publishDiagnostics](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_publishDiagnostics)
* Quick fixes — [codeActionProvider](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_codeAction)
* Document formatting — [documentFormattingProvider](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting)
* Code completions — [completionProvider](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion)
* Function signature help — [signatureHelpProvider](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp)
* Go to definition — [definitionProvider](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition)
* Go to definition — [definitionProvider](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition)
* Go to references — [referencesProvider](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references)
* Document symbols — [documentSymbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol)

## Clients
MATLAB language server supports these editors by installing the corresponding extension:
Expand All @@ -19,4 +20,4 @@ MATLAB language server supports these editors by installing the corresponding ex
### 1.0.0
Release date: 2023-04-26

* Initial release.
* Initial release.
15 changes: 15 additions & 0 deletions src/indexing/FileInfoIndex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,12 @@ export class MatlabClassInfo {
this.baseClasses = rawClassInfo.baseClasses
this.range = convertRange(rawClassInfo.range)
this.declaration = convertRange(rawClassInfo.declaration)

// Since this is the classdef, we'll update all members. Clear them out here.
this.enumerations.clear()
this.properties.clear()
this.methods.clear()
this.parsePropertiesAndEnums(rawClassInfo)
} else {
// Data contains supplementary class info - nothing to do in this situation
}
Expand Down Expand Up @@ -366,6 +372,15 @@ export class MatlabCodeData {
return this.classInfo != null
}

/**
* Whether or not the code data represents a main classdef file.
* For @aclass/aclass.m this returns true
* For @aclass/amethod.m this returns false.
*/
get isMainClassDefDocument (): boolean {
return this.isClassDef && this.uri === this.classInfo?.uri
}

/**
* Finds the info for the function containing the given position.
*
Expand Down
47 changes: 45 additions & 2 deletions src/lifecycle/MatlabLifecycleManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,41 @@ class MatlabLifecycleManager {
return null
}

/**
* Gets the active connection to MATLAB or waits for one to be established.
* Does not attempt to create a connection if one does not currently exist.
* Immediately returns null if the user set the MATLAB connection timing to
* never.
*
* @returns The connection to MATLAB, or null if connection timing is never
*/
async getMatlabConnectionAsync (): Promise<MatlabConnection | null> {
if (await this._isMatlabConnectionTimingNever()) {
return null
}
const isMatlabReady = this._matlabProcess?.isMatlabReady() ?? false
if (isMatlabReady) {
const conn = this._matlabProcess?.getConnection()
if (conn !== null && conn !== undefined) {
return conn
}
}
const result = new Promise<MatlabConnection>((resolve, reject) => {
this.addMatlabLifecycleListener((error, evt) => {
if (error !== null) {
reject(error)
}
if (evt.matlabStatus === 'connected') {
const conn = this.getMatlabConnection()
if (conn !== null) {
resolve(conn)
}
}
})
})
return await result
}

/**
* Gets the active connection to MATLAB. If one does not currently exist, this will
* attempt to establish a connection.
Expand All @@ -105,8 +140,7 @@ class MatlabLifecycleManager {
}

// No active connection - should create a connection if desired
const connectionTiming = (await ConfigurationManager.getConfiguration()).matlabConnectionTiming
if (connectionTiming !== ConnectionTiming.Never) {
if (await this._isMatlabConnectionTimingNever()) {
const matlabProcess = await this.connectToMatlab(connection)
return matlabProcess.getConnection()
}
Expand Down Expand Up @@ -198,6 +232,15 @@ class MatlabLifecycleManager {
})
})
}

/**
*
* @returns True if the MATLAB connection timing setting is set to never. Returns false otherwise.
*/
private async _isMatlabConnectionTimingNever (): Promise<boolean> {
const connectionTiming = (await ConfigurationManager.getConfiguration()).matlabConnectionTiming
return connectionTiming === ConnectionTiming.Never
}
}

/**
Expand Down
5 changes: 3 additions & 2 deletions src/logging/TelemetryUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export enum Actions {
ShutdownMatlab = 'shutdownMATLAB',
FormatDocument = 'formatDocument',
GoToReference = 'goToReference',
GoToDefinition = 'goToDefinition'
GoToDefinition = 'goToDefinition',
DocumentSymbol = 'documentSymbol'
}

export enum ActionErrorConditions {
Expand Down Expand Up @@ -48,7 +49,7 @@ export function reportTelemetryAction(actionType: string, data = ''): void {

/**
* Reports telemetry about a settings change
*
*
* @param settingName The setting's name
* @param newValue The new value
* @param oldValue The old value
Expand Down
82 changes: 78 additions & 4 deletions src/providers/navigation/NavigationSupportProvider.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright 2022 - 2023 The MathWorks, Inc.

import { DefinitionParams, Location, Position, Range, ReferenceParams, TextDocuments } from 'vscode-languageserver'
import { DefinitionParams, DocumentSymbolParams, Location, Position, Range, ReferenceParams, SymbolInformation, SymbolKind, TextDocuments } from 'vscode-languageserver'
import { TextDocument } from 'vscode-languageserver-textdocument'
import { URI } from 'vscode-uri'
import * as fs from 'fs/promises'
Expand Down Expand Up @@ -59,11 +59,24 @@ class Expression {

export enum RequestType {
Definition,
References
References,
DocumentSymbol
}

function reportTelemetry (type: RequestType, errorCondition = '') {
reportTelemetryAction(type === RequestType.Definition ? Actions.GoToDefinition : Actions.GoToReference, errorCondition)
function reportTelemetry (type: RequestType, errorCondition = ''): void {
let action: Actions
switch (type) {
case RequestType.Definition:
action = Actions.GoToDefinition
break
case RequestType.References:
action = Actions.GoToReference
break
case RequestType.DocumentSymbol:
action = Actions.DocumentSymbol
break
}
reportTelemetryAction(action, errorCondition)
}

/**
Expand Down Expand Up @@ -113,6 +126,67 @@ class NavigationSupportProvider {
}
}

/**
*
* @param params Parameters for the document symbol request
* @param documentManager The text document manager
* @param requestType The type of request
* @returns Array of symbols found in the document
*/
async handleDocumentSymbol (params: DocumentSymbolParams, documentManager: TextDocuments<TextDocument>, requestType: RequestType): Promise<SymbolInformation[]> {
// Get or wait for MATLAB connection to handle files opened before MATLAB is ready.
// Calling getOrCreateMatlabConnection would effectively make the onDemand launch
// setting act as onStart.
const matlabConnection = await MatlabLifecycleManager.getMatlabConnectionAsync()
if (matlabConnection == null) {
reportTelemetry(requestType, ActionErrorConditions.MatlabUnavailable)
return []
}

const uri = params.textDocument.uri
const textDocument = documentManager.get(uri)

if (textDocument == null) {
reportTelemetry(requestType, 'No document')
return []
}
await Indexer.indexDocument(textDocument)
let codeData = FileInfoIndex.codeDataCache.get(uri)
if (codeData === null) {
// Ask to index file
await Indexer.indexDocument(textDocument)
codeData = FileInfoIndex.codeDataCache.get(uri)
}
if (codeData == null) {
reportTelemetry(requestType, 'No code data')
return []
}
// Result symbols in documented
const result: SymbolInformation[] = []
// Avoid duplicates coming from different data sources
const visitedRanges: Set<Range> = new Set()
/**
* Push symbol info to result set
*/
function pushSymbol (name: string, kind: SymbolKind, symbolRange: Range): void {
if (!visitedRanges.has(symbolRange)) {
result.push(SymbolInformation.create(name, kind, symbolRange, uri))
visitedRanges.add(symbolRange)
}
}
if (codeData.isMainClassDefDocument && codeData.classInfo != null) {
const classInfo = codeData.classInfo
if (codeData.classInfo.range != null) {
pushSymbol(classInfo.name, SymbolKind.Class, codeData.classInfo.range)
}
classInfo.methods.forEach((info, name) => pushSymbol(name, SymbolKind.Method, info.range))
classInfo.enumerations.forEach((info, name) => pushSymbol(name, SymbolKind.EnumMember, info.range))
classInfo.properties.forEach((info, name) => pushSymbol(name, SymbolKind.Property, info.range))
}
codeData.functions.forEach((info, name) => pushSymbol(name, info.isClassMethod ? SymbolKind.Method : SymbolKind.Function, info.range))
return result
}

/**
* Gets the definition/references request target expression.
*
Expand Down
11 changes: 9 additions & 2 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ connection.onInitialize((params: InitializeParams) => {
referencesProvider: true,
signatureHelpProvider: {
triggerCharacters: ['(', ',']
}
},
documentSymbolProvider: true
}
}

Expand Down Expand Up @@ -164,11 +165,17 @@ connection.onReferences(async params => {
return await NavigationSupportProvider.handleDefOrRefRequest(params, documentManager, RequestType.References)
})

connection.onDocumentSymbol(async params => {
// We return an unawaited promise here to rate limit the number of open requests from the client
// eslint-disable-next-line @typescript-eslint/return-await
return NavigationSupportProvider.handleDocumentSymbol(params, documentManager, RequestType.DocumentSymbol)
})

// Start listening to open/change/close text document events
documentManager.listen(connection)

/** -------------------- Helper Functions -------------------- **/
function reportFileOpened(document: TextDocument) {
function reportFileOpened (document: TextDocument): void {
const roughSize = Math.ceil(document.getText().length / 1024) // in KB
reportTelemetryAction(Actions.OpenFile, roughSize.toString())
}
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"target": "ES6",
"rootDir": "./src",
"outDir": "./out",
"sourceMap": true,
"lib": [
"ES6"
],
Expand Down