1 module served.commands.code_actions; 2 3 import served.extension; 4 import served.types; 5 import served.utils.fibermanager; 6 7 import workspaced.api; 8 import workspaced.com.dcdext; 9 import workspaced.com.importer; 10 import workspaced.coms; 11 12 import served.commands.format : generateDfmtArgs; 13 14 import served.linters.dscanner : DScannerDiagnosticSource, SyntaxHintDiagnosticSource; 15 import served.linters.dub : DubDiagnosticSource; 16 17 import std.algorithm : canFind, map, min, sort, startsWith, uniq; 18 import std.array : array; 19 import std.conv : to; 20 import std.experimental.logger; 21 import std.format : format; 22 import std.json : JSONType, JSONValue; 23 import std.path : buildNormalizedPath, isAbsolute; 24 import std.regex : matchFirst, regex, replaceAll; 25 import std..string : indexOf, indexOfAny, join, replace, strip; 26 27 import fs = std.file; 28 import io = std.stdio; 29 30 package auto importRegex = regex(`import\s+(?:[a-zA-Z_]+\s*=\s*)?([a-zA-Z_]\w*(?:\.\w*[a-zA-Z_]\w*)*)?(\s*\:\s*(?:[a-zA-Z_,\s=]*(?://.*?[\r\n]|/\*.*?\*/|/\+.*?\+/)?)+)?;?`); 31 package static immutable regexQuoteChars = "['\"`]?"; 32 package auto undefinedIdentifier = regex(`^undefined identifier ` ~ regexQuoteChars ~ `(\w+)` 33 ~ regexQuoteChars ~ `(?:, did you mean .*? ` ~ regexQuoteChars ~ `(\w+)` 34 ~ regexQuoteChars ~ `\?)?$`); 35 package auto undefinedTemplate = regex( 36 `template ` ~ regexQuoteChars ~ `(\w+)` ~ regexQuoteChars ~ ` is not defined`); 37 package auto noProperty = regex(`^no property ` ~ regexQuoteChars ~ `(\w+)` 38 ~ regexQuoteChars ~ `(?: for type ` ~ regexQuoteChars ~ `.*?` ~ regexQuoteChars ~ `)?$`); 39 package auto moduleRegex = regex( 40 `(?<!//.*)\bmodule\s+([a-zA-Z_]\w*\s*(?:\s*\.\s*[a-zA-Z_]\w*)*)\s*;`); 41 package auto whitespace = regex(`\s*`); 42 43 @protocolMethod("textDocument/codeAction") 44 CodeAction[] provideCodeActions(CodeActionParams params) 45 { 46 auto document = documents[params.textDocument.uri]; 47 auto instance = activeInstance = backend.getBestInstance(document.uri.uriToFile); 48 if (document.getLanguageId != "d" || !instance) 49 return []; 50 51 // eagerly load DCD in opened files which request code actions 52 if (instance.has!DCDComponent) 53 instance.get!DCDComponent(); 54 55 CodeAction[] ret; 56 if (instance.has!DCDExtComponent) // check if extends 57 { 58 scope codeText = document.rawText.idup; 59 auto startIndex = document.positionToBytes(params.range.start); 60 ptrdiff_t idx = min(cast(ptrdiff_t) startIndex, cast(ptrdiff_t) codeText.length - 1); 61 while (idx > 0) 62 { 63 if (codeText[idx] == ':') 64 { 65 // probably extends 66 if (instance.get!DCDExtComponent.implementAll(codeText, 67 cast(int) startIndex).getYield.length > 0) 68 ret ~= CodeAction(Command("Implement base classes/interfaces", "code-d.implementMethods", 69 [JSONValue(document.positionToOffset(params.range.start))])); 70 break; 71 } 72 if (codeText[idx] == ';' || codeText[idx] == '{' || codeText[idx] == '}') 73 break; 74 idx--; 75 } 76 } 77 foreach (diagnostic; params.context.diagnostics) 78 { 79 if (diagnostic.source == DubDiagnosticSource) 80 { 81 addDubDiagnostics(ret, instance, document, diagnostic, params); 82 } 83 else if (diagnostic.source == DScannerDiagnosticSource) 84 { 85 addDScannerDiagnostics(ret, instance, document, diagnostic, params); 86 } 87 else if (diagnostic.source == SyntaxHintDiagnosticSource) 88 { 89 addSyntaxDiagnostics(ret, instance, document, diagnostic, params); 90 } 91 } 92 return ret; 93 } 94 95 void addDubDiagnostics(ref CodeAction[] ret, WorkspaceD.Instance instance, 96 Document document, Diagnostic diagnostic, CodeActionParams params) 97 { 98 auto match = diagnostic.message.matchFirst(importRegex); 99 if (diagnostic.message.canFind("import ") && match) 100 { 101 ret ~= CodeAction(Command("Import " ~ match[1], "code-d.addImport", 102 [ 103 JSONValue(match[1]), 104 JSONValue(document.positionToOffset(params.range[0])) 105 ])); 106 } 107 if (cast(bool)(match = diagnostic.message.matchFirst(undefinedIdentifier)) 108 || cast(bool)(match = diagnostic.message.matchFirst(undefinedTemplate)) 109 || cast(bool)(match = diagnostic.message.matchFirst(noProperty))) 110 { 111 string[] files; 112 string[] modules; 113 int lineNo; 114 joinAll({ 115 files ~= instance.get!DscannerComponent.findSymbol(match[1]) 116 .getYield.map!"a.file".array; 117 }, { 118 if (instance.has!DCDComponent) 119 files ~= instance.get!DCDComponent.searchSymbol(match[1]).getYield.map!"a.file".array; 120 } /*, { 121 struct Symbol 122 { 123 string project, package_; 124 } 125 126 StopWatch sw; 127 bool got; 128 Symbol[] symbols; 129 sw.start(); 130 info("asking the interwebs for ", match[1]); 131 new Thread({ 132 import std.net.curl : get; 133 import std.uri : encodeComponent; 134 135 auto str = get( 136 "https://symbols.webfreak.org/symbols?limit=60&identifier=" ~ encodeComponent(match[1])); 137 foreach (symbol; parseJSON(str).array) 138 symbols ~= Symbol(symbol["project"].str, symbol["package"].str); 139 got = true; 140 }).start(); 141 while (sw.peek < 3.seconds && !got) 142 Fiber.yield(); 143 foreach (v; symbols.sort!"a.project < b.project" 144 .uniq!"a.project == b.project") 145 ret ~= Command("Import " ~ v.package_ ~ " from dub package " ~ v.project); 146 }*/ 147 ); 148 info("Files: ", files); 149 foreach (file; files.sort().uniq) 150 { 151 if (!isAbsolute(file)) 152 file = buildNormalizedPath(instance.cwd, file); 153 if (!fs.exists(file)) 154 continue; 155 lineNo = 0; 156 foreach (line; io.File(file).byLine) 157 { 158 if (++lineNo >= 100) 159 break; 160 auto match2 = line.matchFirst(moduleRegex); 161 if (match2) 162 { 163 modules ~= match2[1].replaceAll(whitespace, "").idup; 164 break; 165 } 166 } 167 } 168 foreach (mod; modules.sort().uniq) 169 ret ~= CodeAction(Command("Import " ~ mod, "code-d.addImport", [ 170 JSONValue(mod), 171 JSONValue(document.positionToOffset(params.range[0])) 172 ])); 173 } 174 175 if (diagnostic.message.startsWith("use `is` instead of `==`", 176 "use `!is` instead of `!=`") 177 && diagnostic.range.end.character - diagnostic.range.start.character == 2) 178 { 179 auto b = document.positionToBytes(diagnostic.range.start); 180 auto text = document.rawText[b .. $]; 181 182 string target = diagnostic.message[5] == '!' ? "!=" : "=="; 183 string replacement = diagnostic.message[5] == '!' ? "!is" : "is"; 184 185 if (text.startsWith(target)) 186 { 187 string title = format!"Change '%s' to '%s'"(target, replacement); 188 TextEditCollection[DocumentUri] changes; 189 changes[document.uri] = [TextEdit(diagnostic.range, replacement)]; 190 auto action = CodeAction(title, WorkspaceEdit(changes)); 191 action.isPreferred = true; 192 action.diagnostics = [diagnostic]; 193 action.kind = CodeActionKind.quickfix; 194 ret ~= action; 195 } 196 } 197 } 198 199 void addDScannerDiagnostics(ref CodeAction[] ret, WorkspaceD.Instance instance, 200 Document document, Diagnostic diagnostic, CodeActionParams params) 201 { 202 import dscanner.analysis.imports_sortedness : ImportSortednessCheck; 203 204 string key = diagnostic.code.type == JSONType..string ? diagnostic.code.str : null; 205 206 info("Diagnostic: ", diagnostic); 207 208 if (key == ImportSortednessCheck.KEY) 209 { 210 ret ~= CodeAction(Command("Sort imports", "code-d.sortImports", 211 [JSONValue(document.positionToOffset(params.range[0]))])); 212 } 213 214 if (key.length) 215 { 216 if (key.startsWith("dscanner.")) 217 key = key["dscanner.".length .. $]; 218 ret ~= CodeAction(Command("Ignore " ~ key ~ " warnings (this line)", 219 "code-d.ignoreDscannerKey", [diagnostic.code, JSONValue("line")])); 220 ret ~= CodeAction(Command("Ignore " ~ key ~ " warnings", "code-d.ignoreDscannerKey", [ 221 diagnostic.code 222 ])); 223 } 224 } 225 226 void addSyntaxDiagnostics(ref CodeAction[] ret, WorkspaceD.Instance instance, 227 Document document, Diagnostic diagnostic, CodeActionParams params) 228 { 229 string key = diagnostic.code.type == JSONType..string ? diagnostic.code.str : null; 230 switch (key) 231 { 232 case "workspaced.foreach-auto": 233 auto b = document.positionToBytes(diagnostic.range.start); 234 auto text = document.rawText[b .. $]; 235 236 if (text.startsWith("auto")) 237 { 238 auto range = diagnostic.range; 239 size_t offset = 4; 240 foreach (i, dchar c; text[4 .. $]) 241 { 242 offset = 4 + i; 243 if (!isDIdentifierSeparatingChar(c)) 244 break; 245 } 246 range.end = range.start; 247 range.end.character += offset; 248 string title = "Remove 'auto' to fix syntax"; 249 TextEditCollection[DocumentUri] changes; 250 changes[document.uri] = [TextEdit(range, "")]; 251 auto action = CodeAction(title, WorkspaceEdit(changes)); 252 action.isPreferred = true; 253 action.diagnostics = [diagnostic]; 254 action.kind = CodeActionKind.quickfix; 255 ret ~= action; 256 } 257 break; 258 default: 259 warning("No diagnostic fix for our own diagnostic: ", diagnostic); 260 break; 261 } 262 } 263 264 /// Command to sort all user imports in a block at a given position in given code. Returns a list of changes to apply. (Replaces the whole block currently if anything changed, otherwise empty) 265 @protocolMethod("served/sortImports") 266 TextEdit[] sortImports(SortImportsParams params) 267 { 268 auto document = documents[params.textDocument.uri]; 269 TextEdit[] ret; 270 auto sorted = backend.get!ImporterComponent.sortImports(document.rawText, 271 cast(int) document.offsetToBytes(params.location)); 272 if (sorted == ImportBlock.init) 273 return ret; 274 auto start = document.bytesToPosition(sorted.start); 275 auto end = document.movePositionBytes(start, sorted.start, sorted.end); 276 auto lines = sorted.imports.to!(string[]); 277 if (!lines.length) 278 return null; 279 foreach (ref line; lines[1 .. $]) 280 line = sorted.indentation ~ line; 281 string code = lines.join(document.eolAt(0).toString); 282 return [TextEdit(TextRange(start, end), code)]; 283 } 284 285 /// Flag to make dcdext.implementAll return snippets 286 __gshared bool implementInterfaceSnippets; 287 288 /// Implements the interfaces or abstract classes of a specified class/interface. The given position must be on/inside the identifier of any subclass after the colon (`:`) in a class definition. 289 @protocolMethod("served/implementMethods") 290 TextEdit[] implementMethods(ImplementMethodsParams params) 291 { 292 import std.ascii : isWhite; 293 294 auto document = documents[params.textDocument.uri]; 295 string file = document.uri.uriToFile; 296 auto config = workspace(params.textDocument.uri).config; 297 TextEdit[] ret; 298 auto location = document.offsetToBytes(params.location); 299 scope codeText = document.rawText.idup; 300 301 if (gFormattingOptionsApplyOn != params.textDocument.uri) 302 tryFindFormattingSettings(config, document); 303 304 auto eol = document.eolAt(0); 305 auto eolStr = eol.toString; 306 auto toImplement = backend.best!DCDExtComponent(file).implementAll(codeText, cast(int) location, 307 config.d.enableFormatting, generateDfmtArgs(config, eol), implementInterfaceSnippets) 308 .getYield; 309 if (!toImplement.length) 310 return ret; 311 312 string formatCode(ImplementedMethod method) 313 { 314 // cool! snippets handle indentation and new lines automatically so we just keep it as is 315 return method.code; 316 } 317 318 auto existing = backend.best!DCDExtComponent(file).getInterfaceDetails(file, 319 codeText, cast(int) location); 320 if (existing == InterfaceDetails.init) 321 { 322 // insert at start (could not parse class properly) 323 auto brace = codeText.indexOf('{', location); 324 if (brace == -1) 325 brace = codeText.length; 326 brace++; 327 auto pos = document.bytesToPosition(brace); 328 return [ 329 TextEdit(TextRange(pos, pos), 330 eolStr ~ eolStr ~ toImplement.map!(a => formatCode(a)).join(eolStr ~ eolStr)) 331 ]; 332 } 333 else if (existing.methods.length == 0) 334 { 335 // insert at end (no methods in class) 336 auto end = document.bytesToPosition(existing.blockRange[1] - 1); 337 return [ 338 TextEdit(TextRange(end, end), eolStr ~ toImplement.map!(a => formatCode(a)) 339 .join(eolStr ~ eolStr) ~ eolStr) 340 ]; 341 } 342 else 343 { 344 // simply insert at the end of methods, maybe we want to add sorting? 345 // ... ofc that would need a configuration flag because once this is in for a while at least one user will have get used to this and wants to continue having it. 346 auto end = document.bytesToPosition(existing.methods[$ - 1].blockRange[1]); 347 return [ 348 TextEdit(TextRange(end, end), 349 eolStr ~ eolStr ~ toImplement.map!(a => formatCode(a)).join(eolStr ~ eolStr) ~ eolStr) 350 ]; 351 } 352 }