github.com/tiagovtristao/plz@v13.4.0+incompatible/tools/build_langserver/langserver/workspace_store.go (about)

     1  package langserver
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  	"sync"
     7  
     8  	"github.com/thought-machine/please/tools/build_langserver/lsp"
     9  )
    10  
    11  type workspaceStore struct {
    12  	rootURI lsp.DocumentURI
    13  	mu      sync.Mutex
    14  
    15  	documents map[lsp.DocumentURI]*document
    16  }
    17  
    18  type document struct {
    19  	// text content of the document from the last time when saved
    20  	text []string
    21  	// test content of the document while in editing(not been saved)
    22  	textInEdit []string
    23  	version    int
    24  }
    25  
    26  func newWorkspaceStore(rootURI lsp.DocumentURI) *workspaceStore {
    27  	return &workspaceStore{
    28  		rootURI:   rootURI,
    29  		documents: make(map[lsp.DocumentURI]*document),
    30  	}
    31  }
    32  
    33  // Store method is generally used to correspond to "textDocument/didOpen",
    34  // this stores the initial state of the document when opened
    35  func (ws *workspaceStore) Store(uri lsp.DocumentURI, content string, version int) {
    36  	text := SplitLines(content, true)
    37  	ws.documents[uri] = &document{
    38  		text:       text,
    39  		textInEdit: text,
    40  		version:    version,
    41  	}
    42  }
    43  
    44  // Update method corresponds to "textDocument/didSave".
    45  // This updates the existing uri document in the store
    46  func (ws *workspaceStore) Update(uri lsp.DocumentURI, content string) error {
    47  	text := SplitLines(content, true)
    48  	if _, ok := ws.documents[uri]; !ok {
    49  		return fmt.Errorf("document %s did not open", uri)
    50  	}
    51  	ws.documents[uri].text = text
    52  	ws.documents[uri].textInEdit = text
    53  
    54  	return nil
    55  }
    56  
    57  // Close method corresponds to "textDocument/didClose"
    58  // This removes the document stored in workspaceStore
    59  func (ws *workspaceStore) Close(uri lsp.DocumentURI) error {
    60  	if _, ok := ws.documents[uri]; !ok {
    61  		return fmt.Errorf("document %s did not open", uri)
    62  	}
    63  
    64  	ws.mu.Lock()
    65  	defer ws.mu.Unlock()
    66  
    67  	delete(ws.documents, uri)
    68  
    69  	return nil
    70  }
    71  
    72  // TrackEdit tracks the changes of the content for the targeting uri, and update the corresponding
    73  func (ws *workspaceStore) TrackEdit(uri lsp.DocumentURI, contentChanges []lsp.TextDocumentContentChangeEvent, version int) error {
    74  	doc, ok := ws.documents[uri]
    75  	if !ok {
    76  		log.Error("document '%s' is not opened, edit did not apply", uri)
    77  		return nil
    78  	}
    79  
    80  	ws.mu.Lock()
    81  	defer ws.mu.Unlock()
    82  
    83  	for _, change := range contentChanges {
    84  		newText, err := ws.applyChange(doc.textInEdit, change)
    85  		if err != nil {
    86  			return err
    87  		}
    88  		doc.textInEdit = newText
    89  	}
    90  	doc.version = version
    91  
    92  	return nil
    93  }
    94  
    95  func (ws *workspaceStore) applyChange(text []string, change lsp.TextDocumentContentChangeEvent) ([]string, error) {
    96  	if change.Range == nil && change.RangeLength == 0 {
    97  		return text, nil // new full content
    98  	}
    99  
   100  	changeText := change.Text
   101  
   102  	startLine := change.Range.Start.Line
   103  	endLine := change.Range.End.Line
   104  	startCol := change.Range.Start.Character
   105  	endCol := change.Range.End.Character
   106  
   107  	var newText string
   108  
   109  	for i, line := range text {
   110  		if i < startLine || i > endLine {
   111  			newText += line
   112  			continue
   113  		}
   114  
   115  		if i == startLine {
   116  			newText += line[:startCol] + changeText
   117  		}
   118  
   119  		if i == endLine {
   120  			// Apparently, when you delete a whole line, intellij plugin sometimes sends the range like so:
   121  			// {startline: deletedline_index, startcol: 0}, {endline: nextline, endcol: len_of_deleted_line}...
   122  			if len(line)-1 < endCol && (len(text) != 1 && len(text[i-1])-1 == endCol) {
   123  				newText += line
   124  			} else {
   125  				newText += line[endCol:]
   126  			}
   127  		}
   128  	}
   129  
   130  	return SplitLines(newText, true), nil
   131  }
   132  
   133  // SplitLines splits a content with \n characters, and returns a slice of string
   134  // if keepEnds is true, all lines will keep it's original splited character
   135  func SplitLines(content string, keepEnds bool) []string {
   136  	splited := strings.Split(content, "\n")
   137  	if !keepEnds {
   138  		return splited
   139  	}
   140  
   141  	for i := range splited {
   142  		// Do not add endline character on the last empty line
   143  		if (i == len(splited)-1 && splited[i] == "") || len(splited) <= 1 {
   144  			continue
   145  		}
   146  		splited[i] += "\n"
   147  	}
   148  
   149  	return splited
   150  }
   151  
   152  // JoinLines concatenate a slice of string, removes the trailing "\n" if hadEnds is true
   153  func JoinLines(text []string, hasEnds bool) string {
   154  	if !hasEnds {
   155  		concat := strings.Join(text, "\n")
   156  		return concat
   157  	}
   158  
   159  	newText := make([]string, len(text))
   160  	for i := range text {
   161  		if i == len(text)-1 && text[i] == "" {
   162  			newText[i] = text[i]
   163  			continue
   164  		}
   165  		newText[i] = strings.TrimSuffix(text[i], "\n")
   166  	}
   167  
   168  	return strings.Join(newText, "\n")
   169  }