github.com/tiagovtristao/plz@v13.4.0+incompatible/tools/build_langserver/langserver/utils.go (about) 1 package langserver 2 3 import ( 4 "bufio" 5 "context" 6 "github.com/thought-machine/please/src/core" 7 "fmt" 8 "github.com/thought-machine/please/src/fs" 9 "io/ioutil" 10 "os" 11 "path" 12 "path/filepath" 13 "regexp" 14 "strings" 15 16 "github.com/thought-machine/please/src/parse/asp" 17 "github.com/thought-machine/please/tools/build_langserver/lsp" 18 ) 19 20 var quoteExp = regexp.MustCompile(`(^("|')([^"]|"")*("|'))`) 21 var strTailExp = regexp.MustCompile(`(("|')([^"]|"")*("|')$)`) 22 var strExp = regexp.MustCompile(`(^("|')([^"]|"")*("|'))`) 23 var buildLabelExp = regexp.MustCompile(`("(\/\/|:)(\w+\/?)*(\w+[:]\w*)?"?$)`) 24 var literalExp = regexp.MustCompile(`(\w*\.?\w*)$`) 25 26 var attrExp = regexp.MustCompile(`(\.[\w]*)$`) 27 var configAttrExp = regexp.MustCompile(`(CONFIG\.[\w]*)$`) 28 var strAttrExp = regexp.MustCompile(`((".*"|'.*')\.\w*)$`) 29 var dictAttrExp = regexp.MustCompile(`({.*}\.\w*)$`) 30 31 // IsURL checks if the documentUri passed has 'file://' prefix 32 func IsURL(uri lsp.DocumentURI) bool { 33 return strings.HasPrefix(string(uri), "file://") 34 } 35 36 // EnsureURL ensures that the documentURI is a valid path in the filesystem and a valid 'file://' URI 37 func EnsureURL(uri lsp.DocumentURI, pathType string) (url lsp.DocumentURI, err error) { 38 documentPath, err := GetPathFromURL(uri, pathType) 39 if err != nil { 40 return "", err 41 } 42 43 return lsp.DocumentURI("file://" + documentPath), nil 44 } 45 46 // GetPathFromURL returns the absolute path of the file which documenURI relates to 47 // it also checks if the file path is valid 48 func GetPathFromURL(uri lsp.DocumentURI, pathType string) (documentPath string, err error) { 49 var pathFromURL string 50 if IsURL(uri) { 51 pathFromURL = strings.TrimPrefix(string(uri), "file://") 52 } else { 53 pathFromURL = string(uri) 54 } 55 56 absPath, err := filepath.Abs(pathFromURL) 57 if err != nil { 58 return "", err 59 } 60 61 if strings.HasPrefix(absPath, core.RepoRoot) { 62 pathType = strings.ToLower(pathType) 63 switch pathType { 64 case "file": 65 if fs.FileExists(absPath) { 66 return absPath, nil 67 } 68 return "", fmt.Errorf("file %s does not exit", pathFromURL) 69 case "path": 70 if fs.PathExists(absPath) { 71 return absPath, nil 72 } 73 return "", fmt.Errorf("path %s does not exit", pathFromURL) 74 default: 75 return "", fmt.Errorf(fmt.Sprintf("invalid pathType %s, "+ 76 "can only be 'file' or 'path'", pathType)) 77 } 78 } 79 80 return "", fmt.Errorf(fmt.Sprintf("invalid path %s, path must be in repo root: %s", absPath, core.RepoRoot)) 81 } 82 83 // LocalFilesFromURI returns a slices of file path of the files in current directory 84 // where the document is 85 func LocalFilesFromURI(uri lsp.DocumentURI) ([]string, error) { 86 fp, err := GetPathFromURL(uri, "file") 87 if err != nil { 88 return nil, err 89 } 90 91 var files []string 92 93 f, err := ioutil.ReadDir(filepath.Dir(fp)) 94 fname := filepath.Base(fp) 95 for _, i := range f { 96 if i.Name() != "." && i.Name() != fname { 97 files = append(files, i.Name()) 98 } 99 } 100 101 return files, err 102 } 103 104 // PackageLabelFromURI returns a build label of a package 105 func PackageLabelFromURI(uri lsp.DocumentURI) (string, error) { 106 filePath, err := GetPathFromURL(uri, "file") 107 if err != nil { 108 return "", err 109 } 110 pathDir := path.Dir(strings.TrimPrefix(filePath, core.RepoRoot)) 111 112 return "/" + pathDir, nil 113 } 114 115 // ReadFile takes a DocumentURI and reads the file into a slice of string 116 func ReadFile(ctx context.Context, uri lsp.DocumentURI) ([]string, error) { 117 getLines := func(scanner *bufio.Scanner) ([]string, error) { 118 var lines []string 119 120 for scanner.Scan() { 121 select { 122 case <-ctx.Done(): 123 log.Info("process cancelled.") 124 return nil, nil 125 default: 126 lines = append(lines, scanner.Text()) 127 } 128 } 129 130 return lines, scanner.Err() 131 } 132 133 return doIOScan(uri, getLines) 134 135 } 136 137 // GetLineContent returns a []string contraining a single string value respective to position.Line 138 func GetLineContent(ctx context.Context, uri lsp.DocumentURI, position lsp.Position) ([]string, error) { 139 getLine := func(scanner *bufio.Scanner) ([]string, error) { 140 lineCount := 0 141 142 for scanner.Scan() { 143 select { 144 case <-ctx.Done(): 145 log.Info("process cancelled.") 146 return nil, nil 147 default: 148 if lineCount == position.Line { 149 return []string{scanner.Text()}, nil 150 } 151 lineCount++ 152 } 153 } 154 155 return nil, scanner.Err() 156 } 157 158 return doIOScan(uri, getLine) 159 } 160 161 func doIOScan(uri lsp.DocumentURI, callback func(scanner *bufio.Scanner) ([]string, error)) ([]string, error) { 162 filePath, err := GetPathFromURL(uri, "file") 163 if err != nil { 164 return nil, err 165 } 166 167 file, err := os.Open(filePath) 168 if err != nil { 169 return nil, err 170 } 171 172 defer file.Close() 173 174 scanner := bufio.NewScanner(file) 175 176 return callback(scanner) 177 } 178 179 // TrimQuotes is used to trim the qouted string 180 // This is usually used to trim the quoted string in BUILD files, such as a BuildLabel 181 // this will also work for string with any extra characters outside of qoutes 182 // like so: "//src/core", 183 func TrimQuotes(str string) string { 184 // Regex match the string starts with qoute("), 185 // this is so that strings like this(visibility = ["//tools/build_langserver/...", "//src/core"]) won't be matched 186 matched := quoteExp.FindString(strings.TrimSpace(str)) 187 if matched != "" { 188 return matched[1 : len(matched)-1] 189 } 190 191 str = strings.Trim(str, `"`) 192 str = strings.Trim(str, `'`) 193 194 return str 195 } 196 197 // ExtractStrTail extracts the string value from a string, 198 // **the string value must be at the end of the string passed in** 199 func ExtractStrTail(str string) string { 200 matched := strTailExp.FindString(strings.TrimSpace(str)) 201 202 if matched != "" { 203 return matched[1 : len(matched)-1] 204 } 205 206 return "" 207 } 208 209 // LooksLikeString returns true if the input string looks like a string 210 func LooksLikeString(str string) bool { 211 return mustMatch(strExp, str) 212 } 213 214 // LooksLikeAttribute returns true if the input string looks like an attribute: "hello". 215 func LooksLikeAttribute(str string) bool { 216 return mustMatch(attrExp, str) 217 } 218 219 // LooksLikeCONFIGAttr returns true if the input string looks like an attribute of CONFIG object: CONFIG.PLZ_VERSION 220 func LooksLikeCONFIGAttr(str string) bool { 221 return mustMatch(configAttrExp, str) 222 } 223 224 // LooksLikeStringAttr returns true if the input string looks like an attribute of string: "hello".format() 225 func LooksLikeStringAttr(str string) bool { 226 return mustMatch(strAttrExp, str) 227 } 228 229 // LooksLikeDictAttr returns true if the input string looks like an attribute of dict 230 // e.g. {"foo": 1, "bar": "baz"}.keys() 231 func LooksLikeDictAttr(str string) bool { 232 return mustMatch(dictAttrExp, str) 233 } 234 235 // ExtractBuildLabel extracts build label from a string. 236 // Beginning of the buildlabel must have a quote 237 // end of the string must not be anything other than quotes or characters 238 func ExtractBuildLabel(str string) string { 239 matched := buildLabelExp.FindString(strings.TrimSpace(str)) 240 241 return strings.Trim(matched, `"`) 242 } 243 244 // ExtractLiteral extra a literal expression such as function name, variable name from a content line 245 func ExtractLiteral(str string) string { 246 trimmed := strings.TrimSpace(str) 247 248 // Ensure the literal we are looking for is not inside of a string 249 singleQuotes := regexp.MustCompile(`'`).FindAllString(trimmed, -1) 250 doubleQuotes := regexp.MustCompile(`"`).FindAllString(trimmed, -1) 251 if len(singleQuotes)%2 != 0 || len(doubleQuotes)%2 != 0 { 252 return "" 253 } 254 255 // Get our literal 256 matched := literalExp.FindString(trimmed) 257 if matched != "" { 258 return matched 259 } 260 261 return "" 262 } 263 264 func mustMatch(re *regexp.Regexp, str string) bool { 265 matched := re.FindString(str) 266 if matched != "" { 267 return true 268 } 269 return false 270 } 271 272 // StringInSlice checks if an item is in a string slice 273 func StringInSlice(strSlice []string, needle string) bool { 274 for _, item := range strSlice { 275 if item == needle { 276 return true 277 } 278 } 279 280 return false 281 } 282 283 // isEmpty checks if the hovered line is empty 284 func isEmpty(lineContent string, pos lsp.Position) bool { 285 return len(lineContent) < pos.Character || strings.TrimSpace(lineContent[:pos.Character]) == "" 286 } 287 288 // withInRange checks if the input asp.Position from lsp is within the range of the Expression 289 func withInRange(exprPos asp.Position, exprEndPos asp.Position, pos lsp.Position) bool { 290 withInLineRange := pos.Line >= exprPos.Line-1 && 291 pos.Line <= exprEndPos.Line-1 292 293 withInColRange := pos.Character >= exprPos.Column-1 && 294 pos.Character <= exprEndPos.Column-1 295 296 onTheSameLine := pos.Line == exprEndPos.Line-1 && 297 pos.Line == exprPos.Line-1 298 299 if !withInLineRange || (onTheSameLine && !withInColRange) { 300 return false 301 } 302 303 if pos.Line == exprPos.Line-1 { 304 return pos.Character >= exprPos.Column-1 305 } 306 307 if pos.Line == exprEndPos.Line-1 { 308 return pos.Character <= exprEndPos.Column-1 309 } 310 311 return true 312 } 313 314 func withInRangeLSP(targetPos lsp.Position, targetEndPos lsp.Position, pos lsp.Position) bool { 315 start := lspPositionToAsp(targetPos) 316 end := lspPositionToAsp(targetEndPos) 317 318 return withInRange(start, end, pos) 319 } 320 321 func lspPositionToAsp(pos lsp.Position) asp.Position { 322 return asp.Position{ 323 Line: pos.Line + 1, 324 Column: pos.Character + 1, 325 } 326 } 327 328 func aspPositionToLsp(pos asp.Position) lsp.Position { 329 return lsp.Position{ 330 Line: pos.Line - 1, 331 Character: pos.Column - 1, 332 } 333 }