Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support out of the project files #160

Merged
merged 10 commits into from
Jun 13, 2021
51 changes: 48 additions & 3 deletions lib/auto-languageclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@ import * as Utils from "./utils"
import { Socket } from "net"
import { LanguageClientConnection } from "./languageclient"
import { ConsoleLogger, FilteredLogger, Logger } from "./logger"
import { LanguageServerProcess, ServerManager, ActiveServer } from "./server-manager.js"
import {
LanguageServerProcess,
ServerManager,
ActiveServer,
normalizePath,
considerAdditionalPath,
} from "./server-manager.js"
import { Disposable, CompositeDisposable, Point, Range, TextEditor } from "atom"
import * as ac from "atom/autocomplete-plus"
import { basename } from "path"
Expand Down Expand Up @@ -308,7 +314,8 @@ export default class AutoLanguageClient {
(e) => this.shouldStartForEditor(e),
(filepath) => this.filterChangeWatchedFiles(filepath),
this.reportBusyWhile,
this.getServerName()
this.getServerName(),
this.determineProjectPath
)
this._serverManager.startListening()
process.on("exit", () => this.exitCleanup.bind(this))
Expand Down Expand Up @@ -390,6 +397,7 @@ export default class AutoLanguageClient {
connection,
capabilities: initializeResponse.capabilities,
disposable: new CompositeDisposable(),
additionalPaths: new Set<string>(),
}
this.postInitialization(newServer)
connection.initialized()
Expand Down Expand Up @@ -477,6 +485,27 @@ export default class AutoLanguageClient {
this.logger.debug(`exit: code ${code} signal ${signal}`)
}

/** (Optional) Finds the project path. If there is a custom logic for finding projects override this method. */
protected determineProjectPath(textEditor: TextEditor): string | null {
const filePath = textEditor.getPath()
// TODO can filePath be null
if (filePath === null || filePath === undefined) {
return null
}
const projectPath = this._serverManager.getNormalizedProjectPaths().find((d) => filePath.startsWith(d))
if (projectPath !== undefined) {
return projectPath
}

const serverWithClaim = this._serverManager
.getActiveServers()
.find((server) => server.additionalPaths?.has(path.dirname(filePath)))
if (serverWithClaim !== undefined) {
return normalizePath(serverWithClaim.projectPath)
}
return null
}

/**
* The function called whenever the spawned server returns `data` in `stderr` Extend (call super.onSpawnStdErrData) or
* override this if you need custom stderr data handling
Expand Down Expand Up @@ -668,7 +697,23 @@ export default class AutoLanguageClient {
}

this.definitions = this.definitions || new DefinitionAdapter()
return this.definitions.getDefinition(server.connection, server.capabilities, this.getLanguageName(), editor, point)
const query = await this.definitions.getDefinition(
server.connection,
server.capabilities,
this.getLanguageName(),
editor,
point
)

if (query !== null && server.additionalPaths !== undefined) {
// populate additionalPaths based on definitions
// Indicates that the language server can support LSP functionality for out of project files indicated by `textDocument/definition` responses.
for (const def of query.definitions) {
considerAdditionalPath(server as ActiveServer & { additionalPaths: Set<string> }, path.dirname(def.path))
}
}

return query
}

// Outline View via LS documentSymbol---------------------------------
Expand Down
25 changes: 15 additions & 10 deletions lib/server-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export interface ActiveServer {
process: LanguageServerProcess
connection: ls.LanguageClientConnection
capabilities: ls.ServerCapabilities
/** Out of project directories that this server can also support. */
additionalPaths?: Set<string>
}

interface RestartCounter {
Expand All @@ -47,7 +49,8 @@ export class ServerManager {
private _startForEditor: (editor: TextEditor) => boolean,
private _changeWatchedFileFilter: (filePath: string) => boolean,
private _reportBusyWhile: ReportBusyWhile,
private _languageServerName: string
private _languageServerName: string,
private _determineProjectPath: (textEditor: TextEditor) => string | null
) {
this.updateNormalizedProjectPaths()
}
Expand Down Expand Up @@ -121,7 +124,7 @@ export class ServerManager {
textEditor: TextEditor,
{ shouldStart }: { shouldStart?: boolean } = { shouldStart: false }
): Promise<ActiveServer | null> {
const finalProjectPath = this.determineProjectPath(textEditor)
const finalProjectPath = this._determineProjectPath(textEditor)
if (finalProjectPath == null) {
// Files not yet saved have no path
return null
Expand Down Expand Up @@ -245,14 +248,6 @@ export class ServerManager {
})
}

public determineProjectPath(textEditor: TextEditor): string | null {
const filePath = textEditor.getPath()
if (filePath == null) {
return null
}
return this._normalizedProjectPaths.find((d) => filePath.startsWith(d)) || null
}

public updateNormalizedProjectPaths(): void {
this._normalizedProjectPaths = atom.project.getPaths().map(normalizePath)
}
Expand Down Expand Up @@ -350,3 +345,13 @@ export function normalizedProjectPathToWorkspaceFolder(normalizedProjectPath: st
export function normalizePath(projectPath: string): string {
return !projectPath.endsWith(path.sep) ? path.join(projectPath, path.sep) : projectPath
}

/** Considers a path for inclusion in `additionalPaths`. */
export function considerAdditionalPath(
server: ActiveServer & { additionalPaths: Set<string> },
additionalPath: string
): void {
if (!additionalPath.startsWith(server.projectPath)) {
server.additionalPaths.add(additionalPath)
}
}
93 changes: 82 additions & 11 deletions test/auto-languageclient.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { TextEditor } from "atom"
import AutoLanguageClient from "../lib/auto-languageclient"
import { projectPathToWorkspaceFolder, ServerManager } from "../lib/server-manager"
import {
projectPathToWorkspaceFolder,
ServerManager,
ActiveServer,
considerAdditionalPath,
normalizePath,
} from "../lib/server-manager"
import { FakeAutoLanguageClient } from "./helpers"
import { dirname } from "path"
import { dirname, join } from "path"

function mockEditor(uri: string, scopeName: string): any {
return {
Expand All @@ -12,20 +19,84 @@ function mockEditor(uri: string, scopeName: string): any {
}
}

function setupClient() {
atom.workspace.getTextEditors().forEach((editor) => editor.destroy())
atom.project.getPaths().forEach((project) => atom.project.removePath(project))
const client = new FakeAutoLanguageClient()
client.activate()
return client
}

function setupServerManager(client = setupClient()) {
/* eslint-disable-next-line dot-notation */
const serverManager = client["_serverManager"]
return serverManager
}

describe("AutoLanguageClient", () => {
describe("determineProjectPath", () => {
it("returns the project path for an internal or an external file in the project", async () => {
if (process.platform === "darwin") {
// there is nothing OS specific about the code. It just hits the limits that MacOS can handle in this test
pending("skipped on MacOS")
return
}
const client = setupClient()
const serverManager = setupServerManager(client)

// "returns null when a single file is open"

let textEditor = (await atom.workspace.open(__filename)) as TextEditor
/* eslint-disable-next-line dot-notation */
expect(client["determineProjectPath"](textEditor)).toBeNull()
textEditor.destroy()

// "returns the project path when a file of that project is open"
const projectPath = __dirname

// gives the open workspace folder
atom.project.addPath(projectPath)
await serverManager.startServer(projectPath)

textEditor = (await atom.workspace.open(__filename)) as TextEditor
/* eslint-disable-next-line dot-notation */
expect(client["determineProjectPath"](textEditor)).toBe(normalizePath(projectPath))
textEditor.destroy()

// "returns the project path when an external file is open and it is not in additional paths"

const externalDir = join(dirname(projectPath), "lib")
const externalFile = join(externalDir, "main.js")

// gives the open workspace folder
atom.project.addPath(projectPath)
await serverManager.startServer(projectPath)

textEditor = (await atom.workspace.open(externalFile)) as TextEditor
/* eslint-disable-next-line dot-notation */
expect(client["determineProjectPath"](textEditor)).toBeNull()
textEditor.destroy()

// "returns the project path when an external file is open and it is in additional paths"

// get server
const server = serverManager.getActiveServers()[0]
expect(typeof server.additionalPaths).toBe("object") // Set()
// add additional path
considerAdditionalPath(server as ActiveServer & { additionalPaths: Set<string> }, externalDir)
expect(server.additionalPaths?.has(externalDir)).toBeTrue()

textEditor = (await atom.workspace.open(externalFile)) as TextEditor
/* eslint-disable-next-line dot-notation */
expect(client["determineProjectPath"](textEditor)).toBe(normalizePath(projectPath))
textEditor.destroy()
})
})
describe("ServerManager", () => {
describe("WorkspaceFolders", () => {
let client: FakeAutoLanguageClient
let serverManager: ServerManager

beforeEach(() => {
atom.workspace.getTextEditors().forEach((editor) => editor.destroy())
atom.project.getPaths().forEach((project) => atom.project.removePath(project))
client = new FakeAutoLanguageClient()
client.activate()

/* eslint-disable-next-line dot-notation */
serverManager = client["_serverManager"]
serverManager = setupServerManager()
})

afterEach(() => {
Expand Down