Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 49 additions & 19 deletions src/providers/codeAction/importCodeAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@ CodeActionProvider.registerCodeAction({

const valueToImport = getValueToImport(valueNode, possibleImport);

const importAlias = edit?.importAlias ? ` as "${edit.importAlias}"` : "";

return CodeActionProvider.getCodeAction(
params,
valueToImport
? `Import '${valueToImport}' from module "${possibleImport.module}"`
: `Import module "${possibleImport.module}"`,
edit ? [edit] : [],
? `Import '${valueToImport}' from module "${possibleImport.module}"${importAlias}`
: `Import module "${possibleImport.module}"${importAlias}`,
edit ? [edit.edit] : [],
);
});
},
Expand All @@ -63,7 +65,7 @@ CodeActionProvider.registerCodeAction({
params.sourceFile,
diagnostic.range,
firstPossibleImport,
);
)?.edit;

if (edit && !edits.find((e) => e.newText === edit.newText)) {
edits.push(edit);
Expand Down Expand Up @@ -93,19 +95,30 @@ function getPossibleImports(

// Add import quick fixes
if (valueNode) {
return possibleImports.filter(
(exposed) =>
exposed.value === valueNode.text ||
((valueNode.type === "upper_case_qid" ||
valueNode.type === "value_qid") &&
exposed.value ===
valueNode.namedChildren[valueNode.namedChildren.length - 1].text &&
exposed.module ===
valueNode.namedChildren
.slice(0, valueNode.namedChildren.length - 2) // Dots are also namedNodes
.map((a) => a.text)
.join("")),
);
return possibleImports.filter((exposed) => {
if (exposed.value === valueNode.text) {
return true;
}

if (
valueNode.type === "upper_case_qid" ||
valueNode.type === "value_qid"
) {
const targetValue =
valueNode.namedChildren[valueNode.namedChildren.length - 1].text;

const targetModule = getTargetModule(valueNode);

return (
exposed.value === targetValue &&
(targetModule.includes(".")
? exposed.module === targetModule
: exposed.module.endsWith(targetModule))
);
}

return false;
});
}

return [];
Expand All @@ -115,14 +128,24 @@ function getEditFromPossibleImport(
sourceFile: ISourceFile,
range: Range,
possibleImport: IPossibleImport,
): TextEdit | undefined {
): { edit: TextEdit; importAlias: string | undefined } | undefined {
const valueNode = TreeUtils.getNamedDescendantForRange(sourceFile, range);

return RefactorEditUtils.addImport(
const targetModule = getTargetModule(valueNode);
const edit = RefactorEditUtils.addImport(
sourceFile.tree,
possibleImport.module,
getValueToImport(valueNode, possibleImport),
targetModule,
);

if (edit) {
return {
edit,
importAlias:
possibleImport.module !== targetModule ? targetModule : undefined,
};
}
}

function getValueToImport(
Expand All @@ -133,3 +156,10 @@ function getValueToImport(
? possibleImport.valueToImport ?? possibleImport.value
: undefined;
}

function getTargetModule(valueNode: SyntaxNode): string {
return valueNode.namedChildren
.slice(0, valueNode.namedChildren.length - 2) // Dots are also namedNodes
.map((a) => a.text)
.join("");
}
64 changes: 47 additions & 17 deletions src/providers/completionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1209,9 +1209,7 @@ export class CompletionProvider {
return result;
}

let alreadyImported = false;

const matchedSourceFiles: ISourceFile[] = [];
const matchedSourceFiles: [ISourceFile, boolean][] = [];

const imports =
sourceFile.symbolLinks
Expand All @@ -1226,32 +1224,57 @@ export class CompletionProvider {
const moduleName =
imp.node.childForFieldName("moduleName")?.text ?? "";

return program.getSourceFileOfImportableModule(
const importSourceFile = program.getSourceFileOfImportableModule(
sourceFile,
moduleName,
);

if (importSourceFile) {
return [importSourceFile, true] as [ISourceFile, boolean];
}

return undefined;
})
.filter(Utils.notUndefined),
);

alreadyImported = true;
} else if (!checker.getAllImports(sourceFile).getModule(targetModule)) {
// Try to find a module that may not be imported
const moduleSourceFile = program.getSourceFileOfImportableModule(
sourceFile,
targetModule,
);
// If it incudes a dot then we don't look for an alias, only an exact module
if (targetModule.includes(".")) {
const moduleSourceFile = program.getSourceFileOfImportableModule(
sourceFile,
targetModule,
);

if (moduleSourceFile) {
matchedSourceFiles.push(moduleSourceFile);
alreadyImported = false;
if (moduleSourceFile) {
matchedSourceFiles.push([moduleSourceFile, false]);
}
} else {
program
.getImportableModules(sourceFile)
.filter(
({ moduleName }) =>
moduleName === targetModule ||
moduleName.endsWith(`.${targetModule}`),
)
.forEach((module) => {
const moduleSourceFile = program.getSourceFile(module.uri);

if (moduleSourceFile) {
matchedSourceFiles.push([moduleSourceFile, false]);
}
});
}
}

// Get exposed values
matchedSourceFiles
.flatMap(ImportUtils.getPossibleImportsOfTree.bind(this))
.forEach((value) => {
.flatMap(([importSourceFile, alreadyImported]) =>
ImportUtils.getPossibleImportsOfTree(importSourceFile).map(
(value) => [value, alreadyImported] as [IPossibleImport, boolean],
),
)
.forEach(([value, alreadyImported]) => {
const type = checker.findType(value.node);
const typeString = checker.typeToString(type, sourceFile);

Expand All @@ -1264,11 +1287,18 @@ export class CompletionProvider {

// Add the import text edit if not imported
if (!alreadyImported) {
const importEdit = RefactorEditUtils.addImport(tree, targetModule);
const importEdit = RefactorEditUtils.addImport(
tree,
value.module,
undefined,
targetModule,
);

if (importEdit) {
const aliasDetail =
targetModule !== value.module ? ` as '${targetModule}'` : "";
additionalTextEdits = [importEdit];
detail = `Auto import module '${targetModule}'`;
detail = `Auto import module '${value.module}'${aliasDetail}`;
}
}

Expand Down
8 changes: 6 additions & 2 deletions src/util/refactorEditUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,12 +206,16 @@ export class RefactorEditUtils {
tree: Tree,
moduleName: string,
valueName?: string,
moduleAlias?: string,
): TextEdit | undefined {
const lastImportNode =
TreeUtils.getLastImportNode(tree) ??
TreeUtils.getModuleNameCommentNode(tree) ??
TreeUtils.getModuleNameNode(tree)?.parent;

const aliasText =
moduleAlias && moduleAlias !== moduleName ? ` as ${moduleAlias}` : "";

return TextEdit.insert(
Position.create(
lastImportNode?.endPosition.row
Expand All @@ -220,8 +224,8 @@ export class RefactorEditUtils {
0,
),
valueName
? `import ${moduleName} exposing (${valueName})\n`
: `import ${moduleName}\n`,
? `import ${moduleName}${aliasText} exposing (${valueName})\n`
: `import ${moduleName}${aliasText}\n`,
);
}

Expand Down
48 changes: 48 additions & 0 deletions test/codeActionTests/importCodeAction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,54 @@ foo = ""
await testCodeAction(source2, [{ title: `Import module "App"` }]);
});

test("add import alias of qualified value", async () => {
const source = `
--@ Test.elm
module Test exposing (..)

func = Foo.foo
--^

--@ App/Foo.elm
module App.Foo exposing (foo)

foo = ""
`;
await testCodeAction(source, [
{ title: `Import module "App.Foo" as "Foo"` },
]);

const source2 = `
--@ Test.elm
module Test exposing (..)

func = Bar.foo
--^

--@ App/Foo/Bar.elm
module App.Foo.Bar exposing (foo)

foo = ""
`;
await testCodeAction(source2, [
{ title: `Import module "App.Foo.Bar" as "Bar"` },
]);

const source3 = `
--@ Test.elm
module Test exposing (..)

func = Foo.foo
--^

--@ App/Foo/Bar.elm
module App.Foo.Bar exposing (foo)

foo = ""
`;
await testCodeAction(source3, []);
});

test("add all missing imports", async () => {
const source = `
--@ Test.elm
Expand Down
Loading