Skip to content

Commit 236e2d1

Browse files
aminyaUziTech
andauthored
feat: add support for workspace folders (#153)
Co-authored-by: Tony Brix <[email protected]>
1 parent d0b2214 commit 236e2d1

9 files changed

+293
-36
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ npm-debug.log
33
build/
44
yarn.lock
55
package-lock.json
6+
.pnpm-debug.log

lib/auto-languageclient.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { ConsoleLogger, FilteredLogger, Logger } from "./logger"
2929
import { LanguageServerProcess, ServerManager, ActiveServer } from "./server-manager.js"
3030
import { Disposable, CompositeDisposable, Point, Range, TextEditor } from "atom"
3131
import * as ac from "atom/autocomplete-plus"
32+
import { basename } from "path"
3233

3334
export { ActiveServer, LanguageClientConnection, LanguageServerProcess }
3435
export type ConnectionType = "stdio" | "socket" | "ipc"
@@ -106,12 +107,13 @@ export default class AutoLanguageClient {
106107

107108
/** (Optional) Return the parameters used to initialize a client - you may want to extend capabilities */
108109
protected getInitializeParams(projectPath: string, lsProcess: LanguageServerProcess): ls.InitializeParams {
110+
const rootUri = Convert.pathToUri(projectPath)
109111
return {
110112
processId: lsProcess.pid,
111113
rootPath: projectPath,
112-
rootUri: Convert.pathToUri(projectPath),
114+
rootUri,
113115
locale: atom.config.get("atom-i18n.locale") || "en",
114-
workspaceFolders: null,
116+
workspaceFolders: [{ uri: rootUri, name: basename(projectPath) }],
115117
// The capabilities supported.
116118
// TODO the capabilities set to false/undefined are TODO. See {ls.ServerCapabilities} for a full list.
117119
capabilities: {
@@ -124,7 +126,7 @@ export default class AutoLanguageClient {
124126
changeAnnotationSupport: undefined,
125127
resourceOperations: ["create", "rename", "delete"],
126128
},
127-
workspaceFolders: false,
129+
workspaceFolders: true,
128130
didChangeConfiguration: {
129131
dynamicRegistration: false,
130132
},
@@ -568,6 +570,8 @@ export default class AutoLanguageClient {
568570
})
569571

570572
ShowDocumentAdapter.attach(server.connection)
573+
574+
server.connection.onWorkspaceFolders(() => this._serverManager.getWorkspaceFolders())
571575
}
572576

573577
public shouldSyncForEditor(editor: TextEditor, projectPath: string): boolean {

lib/languageclient.ts

+21
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,27 @@ export class LanguageClientConnection extends EventEmitter {
281281
this._sendNotification(lsp.DidChangeWatchedFilesNotification.type, params)
282282
}
283283

284+
/**
285+
* Public: Register a callback for the `workspace.workspaceFolders` request. This request is sent from the server to
286+
* Atom to fetch the current open list of workspace folders
287+
*
288+
* @param A Callback which returns a {Promise} containing an {Array} of {lsp.WorkspaceFolder[]} or {null} if only a
289+
* single file is open in the tool.
290+
*/
291+
public onWorkspaceFolders(callback: () => Promise<lsp.WorkspaceFolder[] | null>): void {
292+
return this._onRequest(lsp.WorkspaceFoldersRequest.type, callback)
293+
}
294+
295+
/**
296+
* Public: Send a `workspace/didChangeWorkspaceFolders` notification.
297+
*
298+
* @param {DidChangeWorkspaceFoldersParams} params An object that contains the actual workspace folder change event
299+
* ({WorkspaceFoldersChangeEvent}) in its {event} property
300+
*/
301+
public didChangeWorkspaceFolders(params: lsp.DidChangeWorkspaceFoldersParams): void {
302+
this._sendNotification(lsp.DidChangeWorkspaceFoldersNotification.type, params)
303+
}
304+
284305
/**
285306
* Public: Register a callback for the `textDocument/publishDiagnostics` message.
286307
*

lib/server-manager.ts

+67-8
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Logger } from "./logger"
66
import { CompositeDisposable, FilesystemChangeEvent, TextEditor } from "atom"
77
import { ReportBusyWhile } from "./utils"
88

9-
type MinimalLanguageServerProcess = Pick<ChildProcess, "stdin" | "stdout" | "stderr" | "pid" | "kill" | "on">
9+
export type MinimalLanguageServerProcess = Pick<ChildProcess, "stdin" | "stdout" | "stderr" | "pid" | "kill" | "on">
1010

1111
/**
1212
* Public: Defines a language server process which is either a ChildProcess, or it is a minimal object that resembles a
@@ -38,6 +38,7 @@ export class ServerManager {
3838
private _disposable: CompositeDisposable = new CompositeDisposable()
3939
private _editorToServer: Map<TextEditor, ActiveServer> = new Map()
4040
private _normalizedProjectPaths: string[] = []
41+
private _previousNormalizedProjectPaths: string[] | undefined = undefined // TODO we should not hold a separate cache
4142
private _isStarted = false
4243

4344
constructor(
@@ -60,6 +61,7 @@ export class ServerManager {
6061
this._disposable.add(atom.project.onDidChangeFiles(this.projectFilesChanged.bind(this)))
6162
}
6263
}
64+
this._isStarted = true
6365
}
6466

6567
public stopListening(): void {
@@ -111,8 +113,8 @@ export class ServerManager {
111113
}
112114
}
113115

114-
public getActiveServers(): ActiveServer[] {
115-
return this._activeServers.slice()
116+
public getActiveServers(): Readonly<ActiveServer[]> {
117+
return this._activeServers
116118
}
117119

118120
public async getServer(
@@ -252,17 +254,55 @@ export class ServerManager {
252254
}
253255

254256
public updateNormalizedProjectPaths(): void {
255-
this._normalizedProjectPaths = atom.project.getDirectories().map((d) => this.normalizePath(d.getPath()))
257+
this._normalizedProjectPaths = atom.project.getPaths().map(normalizePath)
256258
}
257259

258-
public normalizePath(projectPath: string): string {
259-
return !projectPath.endsWith(path.sep) ? path.join(projectPath, path.sep) : projectPath
260+
public getNormalizedProjectPaths(): Readonly<string[]> {
261+
return this._normalizedProjectPaths
262+
}
263+
264+
/**
265+
* Public: fetch the current open list of workspace folders
266+
*
267+
* @returns A {Promise} containing an {Array} of {lsp.WorkspaceFolder[]} or {null} if only a single file is open in the tool.
268+
*/
269+
public getWorkspaceFolders(): Promise<ls.WorkspaceFolder[] | null> {
270+
// NOTE the method must return a Promise based on the specification
271+
const projectPaths = this.getNormalizedProjectPaths()
272+
if (projectPaths.length === 0) {
273+
// only a single file is open
274+
return Promise.resolve(null)
275+
} else {
276+
return Promise.resolve(projectPaths.map(normalizedProjectPathToWorkspaceFolder))
277+
}
260278
}
261279

262280
public async projectPathsChanged(projectPaths: string[]): Promise<void> {
263-
const pathsSet = new Set(projectPaths.map(this.normalizePath))
264-
const serversToStop = this._activeServers.filter((s) => !pathsSet.has(s.projectPath))
281+
const pathsAll = projectPaths.map(normalizePath)
282+
283+
const previousPaths = this._previousNormalizedProjectPaths ?? this.getNormalizedProjectPaths()
284+
const pathsRemoved = previousPaths.filter((projectPath) => !pathsAll.includes(projectPath))
285+
const pathsAdded = pathsAll.filter((projectPath) => !previousPaths.includes(projectPath))
286+
287+
// update cache
288+
this._previousNormalizedProjectPaths = pathsAll
289+
290+
// send didChangeWorkspaceFolders
291+
const didChangeWorkspaceFoldersParams = {
292+
event: {
293+
added: pathsAdded.map(normalizedProjectPathToWorkspaceFolder),
294+
removed: pathsRemoved.map(normalizedProjectPathToWorkspaceFolder),
295+
},
296+
}
297+
for (const activeServer of this._activeServers) {
298+
activeServer.connection.didChangeWorkspaceFolders(didChangeWorkspaceFoldersParams)
299+
}
300+
301+
// stop the servers that don't have projectPath
302+
const serversToStop = this._activeServers.filter((server) => pathsRemoved.includes(server.projectPath))
265303
await Promise.all(serversToStop.map((s) => this.stopServer(s)))
304+
305+
// update this._normalizedProjectPaths
266306
this.updateNormalizedProjectPaths()
267307
}
268308

@@ -290,4 +330,23 @@ export class ServerManager {
290330
}
291331
}
292332
}
333+
334+
/** @deprecated Use the exported `normalizePath` function */
335+
public normalizePath = normalizePath
336+
}
337+
338+
export function projectPathToWorkspaceFolder(projectPath: string): ls.WorkspaceFolder {
339+
const normalizedProjectPath = normalizePath(projectPath)
340+
return normalizedProjectPathToWorkspaceFolder(normalizedProjectPath)
341+
}
342+
343+
export function normalizedProjectPathToWorkspaceFolder(normalizedProjectPath: string): ls.WorkspaceFolder {
344+
return {
345+
uri: Convert.pathToUri(normalizedProjectPath),
346+
name: path.basename(normalizedProjectPath),
347+
}
348+
}
349+
350+
export function normalizePath(projectPath: string): string {
351+
return !projectPath.endsWith(path.sep) ? path.join(projectPath, path.sep) : projectPath
293352
}

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,13 @@
3737
},
3838
"devDependencies": {
3939
"@types/atom": "^1.40.10",
40+
"@types/jasmine": "^3.7.1",
4041
"@types/node": "15.0.2",
4142
"atom-jasmine3-test-runner": "^5.2.4",
4243
"eslint-config-atomic": "^1.14.4",
4344
"prettier-config-atomic": "^2.0.5",
4445
"shx": "^0.3.3",
46+
"spawk": "^1.4.0",
4547
"typescript": "~4.2.4"
4648
}
4749
}

pnpm-lock.yaml

+13
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)